From fb4253586eaae7aa7d24d29aa3802bf34de9a39c Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Tue, 10 Sep 2024 23:19:37 -0700 Subject: [PATCH 01/11] separated functions into private and public and added comment based help --- CHANGELOG.md | 20 +++++++ source/Private/Get-ProfilePathFromSID.ps1 | 38 ++++++++++++ source/Private/Get-RegistryKeyForSID.ps1 | 40 +++++++++++++ source/Private/New-UserProfileObject.ps1 | 44 ++++++++++++++ source/Private/Remove-RegistryKeyForSID.ps1 | 45 ++++++++++++++ source/Private/Remove-SIDProfile.ps1 | 53 +++++++++++++++++ source/Private/Test-FolderExists.ps1 | 25 ++++++++ source/Private/Test-OrphanedProfile.ps1 | 46 +++++++++++++++ source/Private/Test-SpecialAccount.ps1 | 43 ++++++++++++++ source/Public/Get-AllUserProfiles.ps1 | 58 ++++++++++++++----- source/Public/Get-OrphanedProfiles.ps1 | 27 ++++++++- source/Public/Get-ProfilePathFromSID.ps1 | 19 ------ source/Public/Get-RegistryKeyForSID.ps1 | 19 ------ source/Public/Get-SIDProfileInfo.ps1 | 29 ++++++++-- source/Public/Get-UserFolders.ps1 | 23 +++++++- source/Public/Get-UserProfilesFromFolders.ps1 | 20 ++++++- .../Public/Get-UserProfilesFromRegistry.ps1 | 17 ++++++ source/Public/New-UserProfileObject.ps1 | 19 ------ source/Public/Remove-OrphanedProfiles.ps1 | 33 +++++++++-- source/Public/Remove-ProfilesForSIDs.ps1 | 46 +++++++++++---- source/Public/Remove-RegistryKeyForSID.ps1 | 22 ------- source/Public/Remove-SIDProfile.ps1 | 31 ---------- source/Public/Test-FolderExists.ps1 | 10 ---- source/Public/Test-OrphanedProfile.ps1 | 20 ------- source/Public/Test-SpecialAccount.ps1 | 26 --------- 25 files changed, 571 insertions(+), 202 deletions(-) create mode 100644 source/Private/Get-ProfilePathFromSID.ps1 create mode 100644 source/Private/Get-RegistryKeyForSID.ps1 create mode 100644 source/Private/New-UserProfileObject.ps1 create mode 100644 source/Private/Remove-RegistryKeyForSID.ps1 create mode 100644 source/Private/Remove-SIDProfile.ps1 create mode 100644 source/Private/Test-FolderExists.ps1 create mode 100644 source/Private/Test-OrphanedProfile.ps1 create mode 100644 source/Private/Test-SpecialAccount.ps1 delete mode 100644 source/Public/Get-ProfilePathFromSID.ps1 delete mode 100644 source/Public/Get-RegistryKeyForSID.ps1 delete mode 100644 source/Public/New-UserProfileObject.ps1 delete mode 100644 source/Public/Remove-RegistryKeyForSID.ps1 delete mode 100644 source/Public/Remove-SIDProfile.ps1 delete mode 100644 source/Public/Test-FolderExists.ps1 delete mode 100644 source/Public/Test-OrphanedProfile.ps1 delete mode 100644 source/Public/Test-SpecialAccount.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 39412e5..6f35385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added core functions - configured `WinRegOps` as a dependant module - Updated build file for release + +- Comment-based help documentation added for the following public functions: + - `Get-AllUserProfiles` + - `Get-OrphanedProfiles` + - `Remove-OrphanedProfiles` + - `Remove-SIDProfile` + - `Get-UserProfileFolders` + - `Get-RegistryUserProfiles` + - `Get-UserFolders` + - `Get-SIDProfileInfo` + +- Comment-based help documentation added for the following public functions: +- `New-UserProfileObject` +- `Remove-RegistryKeyForSID` +- `Remove-ProfilesForSIDs` +- `Get-RegistryKeyForSID` +- `Get-ProfilePathFromSID` +- `Test-FolderExists` +- `Test-OrphanedProfile` +- `Test-SpecialAccount` diff --git a/source/Private/Get-ProfilePathFromSID.ps1 b/source/Private/Get-ProfilePathFromSID.ps1 new file mode 100644 index 0000000..8b1b661 --- /dev/null +++ b/source/Private/Get-ProfilePathFromSID.ps1 @@ -0,0 +1,38 @@ +<# +.SYNOPSIS + Retrieves the profile path associated with a specific SID from the registry. +.DESCRIPTION + The Get-ProfilePathFromSID function retrieves the "ProfileImagePath" registry value for the provided SID registry key. This path indicates the location of the user profile associated with the SID. +.PARAMETER SidKey + The registry key representing the Security Identifier (SID) from which to retrieve the profile path. +.EXAMPLE + Get-ProfilePathFromSID -SidKey $sidKey + Retrieves the profile path for the given SID from the registry. +.NOTES + If the "ProfileImagePath" cannot be found, the function will return `$null` and a verbose message will indicate the issue. + In case of an error during retrieval, an error message is logged and the function returns `$null`. +#> +function Get-ProfilePathFromSID +{ + param ( + [Microsoft.Win32.RegistryKey]$SidKey + ) + + try + { + # Use Get-RegistryValue to retrieve the "ProfileImagePath" + $profileImagePath = Get-RegistryValue -Key $SidKey -ValueName "ProfileImagePath" + + if (-not $profileImagePath) + { + Write-Verbose "ProfileImagePath not found for SID '$($SidKey.Name)'." + } + + return $profileImagePath + } + catch + { + Write-Error "Failed to retrieve ProfileImagePath for SID '$($SidKey.Name)'. Error: $_" + return $null + } +} diff --git a/source/Private/Get-RegistryKeyForSID.ps1 b/source/Private/Get-RegistryKeyForSID.ps1 new file mode 100644 index 0000000..6af2e32 --- /dev/null +++ b/source/Private/Get-RegistryKeyForSID.ps1 @@ -0,0 +1,40 @@ +<# +.SYNOPSIS + Retrieves the registry key associated with a specified SID from the ProfileList. +.DESCRIPTION + The Get-RegistryKeyForSID function attempts to open and retrieve the registry subkey for a given Security Identifier (SID) from the ProfileList. If the SID does not exist or an error occurs while accessing the registry, the function returns `$null` and logs a warning or error message. +.PARAMETER SID + The Security Identifier (SID) for which to retrieve the registry subkey. +.PARAMETER ProfileListKey + The opened registry key representing the ProfileList, which contains the subkeys for user profiles. +.EXAMPLE + Get-RegistryKeyForSID -SID "S-1-5-21-123456789-1001" -ProfileListKey $profileListKey + Retrieves the registry subkey associated with the specified SID from the ProfileList. +.NOTES + If the registry key for the SID cannot be found or accessed, the function returns `$null` and logs an appropriate warning or error message. + The function relies on the Open-RegistrySubKey function to retrieve the subkey. +#> +function Get-RegistryKeyForSID +{ + param ( + [string]$SID, + [Microsoft.Win32.RegistryKey]$ProfileListKey + ) + + try + { + # Use the general Open-RegistrySubKey function to get the subkey for the SID + $sidKey = Open-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID + if ($sidKey -eq $null) + { + Write-Warning "The SID '$SID' does not exist in the ProfileList registry." + return $null + } + return $sidKey + } + catch + { + Write-Error "Error accessing registry key for SID '$SID'. Error: $_" + return $null + } +} diff --git a/source/Private/New-UserProfileObject.ps1 b/source/Private/New-UserProfileObject.ps1 new file mode 100644 index 0000000..d6f5f5d --- /dev/null +++ b/source/Private/New-UserProfileObject.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS + Creates a new UserProfile object. +.DESCRIPTION + The New-UserProfileObject function creates and returns an instance of the UserProfile class. The function takes in various parameters such as SID, profile path, and whether the profile is orphaned or special, and returns a UserProfile object with these details. +.PARAMETER SID + The Security Identifier (SID) of the user profile. +.PARAMETER ProfilePath + The file path to the user profile folder. +.PARAMETER IsOrphaned + A boolean value indicating whether the profile is orphaned (i.e., exists in the registry but not on disk, or vice versa). +.PARAMETER OrphanReason + A description of why the profile is considered orphaned, if applicable. +.PARAMETER ComputerName + The name of the computer where the profile is located. +.PARAMETER IsSpecial + A boolean value indicating whether the profile is for a special account (e.g., system or default accounts). +.EXAMPLE + New-UserProfileObject -SID "S-1-5-21-123456789-1001" -ProfilePath "C:\Users\John" -IsOrphaned $true -OrphanReason "MissingRegistryEntry" -ComputerName "Server01" -IsSpecial $false + Creates a new UserProfile object for the profile associated with the given SID, marking it as orphaned with a reason. +.NOTES + This function returns an instance of the UserProfile class, which is used for managing and reporting on user profiles across different systems. +#> + +function New-UserProfileObject +{ + param ( + [string]$SID, + [string]$ProfilePath, + [bool]$IsOrphaned, + [string]$OrphanReason, + [string]$ComputerName, + [bool]$IsSpecial + ) + + return [UserProfile]::new( + $SID, + $ProfilePath, + $IsOrphaned, + $OrphanReason, + $ComputerName, + $IsSpecial + ) +} diff --git a/source/Private/Remove-RegistryKeyForSID.ps1 b/source/Private/Remove-RegistryKeyForSID.ps1 new file mode 100644 index 0000000..6c0ccfc --- /dev/null +++ b/source/Private/Remove-RegistryKeyForSID.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS + Deletes a registry key associated with a specific SID from the ProfileList. +.DESCRIPTION + The Remove-RegistryKeyForSID function deletes the registry key corresponding to a specific Security Identifier (SID) from the ProfileList in the Windows registry. It supports confirmation prompts and simulates actions with the -WhatIf parameter. +.PARAMETER SID + The Security Identifier (SID) for which the registry key should be deleted. +.PARAMETER ProfileListKey + The opened registry key representing the ProfileList where the profile's SID is located. +.PARAMETER ComputerName + The name of the computer where the profile registry key resides. By default, this is the current computer. +.EXAMPLE + Remove-RegistryKeyForSID -SID "S-1-5-21-123456789-1001" -ProfileListKey $profileListKey -ComputerName "Server01" + Deletes the registry key for the specified SID from the ProfileList on "Server01". +.NOTES + This function supports 'ShouldProcess', so it can be used in conjunction with the -WhatIf or -Confirm parameters to simulate the deletion. + It also includes error handling to ensure any failure during the registry key deletion is captured. +#> + +function Remove-RegistryKeyForSID +{ + #Deletes a single registry key for a SID. + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory = $true)] + [string]$SID, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey]$ProfileListKey, + + [Parameter(Mandatory = $true)] + [string]$ComputerName = $env:COMPUTERNAME + ) + + try + { + # Use the general Remove-RegistrySubKey function to delete the SID's subkey + return Remove-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID -ComputerName $ComputerName + } + catch + { + Write-Error "Failed to remove the profile registry key for SID '$SID' on $ComputerName. Error: $_" + return $false + } +} diff --git a/source/Private/Remove-SIDProfile.ps1 b/source/Private/Remove-SIDProfile.ps1 new file mode 100644 index 0000000..dd35427 --- /dev/null +++ b/source/Private/Remove-SIDProfile.ps1 @@ -0,0 +1,53 @@ +<# +.SYNOPSIS + Removes a profile for a specific SID by deleting its registry key. +.DESCRIPTION + The Remove-SIDProfile function attempts to delete the profile registry key for the specified SID. It returns a ProfileDeletionResult object indicating whether the deletion was successful or not. +.PARAMETER SID + The Security Identifier (SID) for the profile to be removed. +.PARAMETER ProfileListKey + The opened registry key for the ProfileList where the profile's SID is located. +.PARAMETER ComputerName + The name of the computer where the registry key will be deleted. +.PARAMETER ProfilePath + The file path of the profile to be deleted. +.EXAMPLE + Remove-SIDProfile -SID "S-1-5-21-123456789-1001" -ProfileListKey $profileListKey -ComputerName "Server01" -ProfilePath "C:\Users\John" + Removes the profile associated with the specified SID from "Server01". +#> + +function Remove-SIDProfile +{ + #Coordinates the registry key deletion and provides a result for a single SID. + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [string]$SID, + [Microsoft.Win32.RegistryKey]$ProfileListKey, + [string]$ComputerName, + [string]$ProfilePath + ) + + # Attempt to remove the registry key + $deletionSuccess = Remove-RegistryKeyForSID -SID $SID -ProfileListKey $ProfileListKey -ComputerName $ComputerName + + if ($deletionSuccess) + { + return [ProfileDeletionResult]::new( + $SID, + $ProfilePath, + $true, + "Profile registry key for SID '$SID' successfully deleted.", + $ComputerName + ) + } + else + { + return [ProfileDeletionResult]::new( + $SID, + $ProfilePath, + $false, + "Failed to delete the profile registry key for SID '$SID'.", + $ComputerName + ) + } +} diff --git a/source/Private/Test-FolderExists.ps1 b/source/Private/Test-FolderExists.ps1 new file mode 100644 index 0000000..7dee156 --- /dev/null +++ b/source/Private/Test-FolderExists.ps1 @@ -0,0 +1,25 @@ +<# +.SYNOPSIS + Checks if a profile folder exists on a specified computer. +.DESCRIPTION + The Test-FolderExists function determines whether a given profile folder exists on the specified computer by testing the path. +.PARAMETER ProfilePath + The file path of the profile folder to check. +.PARAMETER ComputerName + The name of the computer where the profile folder is located. +.EXAMPLE + Test-FolderExists -ProfilePath "C:\Users\John" -ComputerName "Server01" + Checks if the folder "C:\Users\John" exists on "Server01". +#> + +function Test-FolderExists +{ + param ( + [string]$ProfilePath, + [string]$ComputerName + ) + + $IsLocal = $ComputerName -eq $env:COMPUTERNAME + $pathToCheck = Get-DirectoryPath -BasePath $ProfilePath -ComputerName $ComputerName -IsLocal $IsLocal + return Test-Path $pathToCheck +} diff --git a/source/Private/Test-OrphanedProfile.ps1 b/source/Private/Test-OrphanedProfile.ps1 new file mode 100644 index 0000000..5228ef5 --- /dev/null +++ b/source/Private/Test-OrphanedProfile.ps1 @@ -0,0 +1,46 @@ +<# +.SYNOPSIS + Tests whether a profile is orphaned. +.DESCRIPTION + The Test-OrphanedProfile function checks if a profile is orphaned by evaluating the profile path, folder existence, and whether it's a special account. +.PARAMETER SID + The Security Identifier (SID) of the profile being tested. +.PARAMETER ProfilePath + The file path of the profile folder. +.PARAMETER FolderExists + Indicates whether the profile folder exists on the computer. +.PARAMETER IgnoreSpecial + Switch to ignore special or default profiles when determining if the profile is orphaned. +.PARAMETER IsSpecial + Indicates whether the profile is a special account. +.PARAMETER ComputerName + The name of the computer where the profile is being tested. +.EXAMPLE + Test-OrphanedProfile -SID "S-1-5-21-123456789-1001" -ProfilePath "C:\Users\John" -FolderExists $true -IgnoreSpecial -IsSpecial $false -ComputerName "Server01" + Tests if the profile associated with the given SID is orphaned on "Server01". +#> + +function Test-OrphanedProfile +{ + param ( + [string]$SID, + [string]$ProfilePath, + [bool]$FolderExists, + [bool]$IgnoreSpecial, + [bool]$IsSpecial, + [string]$ComputerName + ) + + if (-not $ProfilePath) + { + return New-UserProfileObject $SID "(null)" $true "MissingProfileImagePath" $ComputerName $IsSpecial + } + elseif (-not $FolderExists) + { + return New-UserProfileObject $SID $ProfilePath $true "MissingFolder" $ComputerName $IsSpecial + } + else + { + return New-UserProfileObject $SID $ProfilePath $false $null $ComputerName $IsSpecial + } +} diff --git a/source/Private/Test-SpecialAccount.ps1 b/source/Private/Test-SpecialAccount.ps1 new file mode 100644 index 0000000..6c191af --- /dev/null +++ b/source/Private/Test-SpecialAccount.ps1 @@ -0,0 +1,43 @@ +<# +.SYNOPSIS + Tests if a profile is a special account. +.DESCRIPTION + The Test-SpecialAccount function checks whether the profile is a special or default account by evaluating the folder name, SID, and profile path against a predefined list of ignored accounts, SIDs, and paths. +.PARAM FolderName + The folder name of the profile being tested. +.PARAM SID + The Security Identifier (SID) of the profile being tested. +.PARAM ProfilePath + The file path of the profile folder. +.EXAMPLE + Test-SpecialAccount -FolderName "DefaultAppPool" -SID "S-1-5-18" -ProfilePath "C:\WINDOWS\system32\config\systemprofile" + Checks if the profile associated with "DefaultAppPool" is a special account. +#> + +function Test-SpecialAccount +{ + param ( + [string]$FolderName, + [string]$SID, + [string]$ProfilePath + ) + + # List of default or special accounts to ignore + $IgnoredAccounts = @( + "defaultuser0", "DefaultAppPool", "servcm12", "Public", "PBIEgwService", "Default", + "All Users", "win2kpro" + ) + $IgnoredSIDs = @( + "S-1-5-18", # Local System + "S-1-5-19", # Local Service + "S-1-5-20" # Network Service + ) + $IgnoredPaths = @( + "C:\WINDOWS\system32\config\systemprofile", # System profile + "C:\WINDOWS\ServiceProfiles\LocalService", # Local service profile + "C:\WINDOWS\ServiceProfiles\NetworkService" # Network service profile + ) + + # Check if the account is special based on the folder name, SID, or profile path + return ($IgnoredAccounts -contains $FolderName) -or ($IgnoredSIDs -contains $SID) -or ($IgnoredPaths -contains $ProfilePath) +} diff --git a/source/Public/Get-AllUserProfiles.ps1 b/source/Public/Get-AllUserProfiles.ps1 index 28d35b0..842c06f 100644 --- a/source/Public/Get-AllUserProfiles.ps1 +++ b/source/Public/Get-AllUserProfiles.ps1 @@ -1,23 +1,49 @@ -function Get-AllUserProfiles { +<# +.SYNOPSIS + Retrieves all user profiles from both the registry and file system on a specified computer. +.DESCRIPTION + The Get-AllUserProfiles function collects user profile information from both the file system (profile folders) and the registry on the specified computer. It compares the two sets of profiles, identifying orphaned profiles that exist in one location but not the other. The function also allows the option to ignore special or default profiles, such as system or service accounts. +.PARAMETER ComputerName + The name of the computer from which to retrieve user profiles. Defaults to the local computer. +.PARAMETER ProfileFolderPath + The folder path where user profiles are stored. Defaults to "$env:SystemDrive\Users". +.PARAMETER IgnoreSpecial + Switch to ignore special or default profiles during the profile retrieval process. +.EXAMPLE + Get-AllUserProfiles -ComputerName "Server01" + Retrieves all user profiles from both the file system and registry on "Server01". +.EXAMPLE + Get-AllUserProfiles -ProfileFolderPath "D:\UserProfiles" -IgnoreSpecial + Retrieves user profiles from the specified folder and ignores special or default profiles. +.NOTES + This function compares user profiles found in the file system and the registry to identify orphaned profiles. + It supports pipeline input for multiple computer names, allowing you to retrieve profiles from multiple systems. + Special or default profiles, such as system accounts, can be ignored by using the -IgnoreSpecial parameter. +#> +function Get-AllUserProfiles +{ [CmdletBinding()] param ( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [string]$ComputerName = $env:COMPUTERNAME, - [string]$ProfileFolderPath = "C:\Users", + [string]$ProfileFolderPath = "$env:SystemDrive\Users", [switch]$IgnoreSpecial ) # Begin block runs once before processing pipeline input - begin { + begin + { # Initialize an array to hold all UserProfile objects across multiple pipeline inputs $AllProfiles = @() } # Process block runs once for each input object (in case of pipeline) - process { + process + { # Test if the computer is online before proceeding - if (-not (Test-ComputerPing -ComputerName $ComputerName)) { + if (-not (Test-ComputerPing -ComputerName $ComputerName)) + { Write-Warning "Computer '$ComputerName' is offline or unreachable." return # Skip to the next input in the pipeline } @@ -27,43 +53,49 @@ function Get-AllUserProfiles { $RegistryProfiles = Get-UserProfilesFromRegistry -ComputerName $ComputerName # Loop through registry profiles and check for folder existence and ProfileImagePath - foreach ($regProfile in $RegistryProfiles) { + foreach ($regProfile in $RegistryProfiles) + { $profilePath = $regProfile.ProfilePath $folderExists = Test-FolderExists -ProfilePath $profilePath -ComputerName $regProfile.ComputerName $folderName = Split-Path -Path $profilePath -Leaf $isSpecial = Test-SpecialAccount -FolderName $folderName -SID $regProfile.SID -ProfilePath $profilePath # Skip special profiles if IgnoreSpecial is set - if ($IgnoreSpecial -and $isSpecial) { + if ($IgnoreSpecial -and $isSpecial) + { continue } # Detect if the profile is orphaned and create the user profile object $userProfile = Test-OrphanedProfile -SID $regProfile.SID -ProfilePath $profilePath ` - -FolderExists $folderExists -IgnoreSpecial $IgnoreSpecial ` - -IsSpecial $isSpecial -ComputerName $ComputerName + -FolderExists $folderExists -IgnoreSpecial $IgnoreSpecial ` + -IsSpecial $isSpecial -ComputerName $ComputerName $AllProfiles += $userProfile } # Loop through user folders and check if they exist in the registry - foreach ($folder in $UserFolders) { + foreach ($folder in $UserFolders) + { $registryProfile = $RegistryProfiles | Where-Object { $_.ProfilePath -eq $folder.ProfilePath } $isSpecial = Test-SpecialAccount -FolderName $folder.FolderName -SID $null -ProfilePath $folder.ProfilePath # Skip special profiles if IgnoreSpecial is set - if ($IgnoreSpecial -and $isSpecial) { + if ($IgnoreSpecial -and $isSpecial) + { continue } # Case 4: Folder exists in C:\Users but not in the registry - if (-not $registryProfile) { + if (-not $registryProfile) + { $AllProfiles += New-UserProfileObject $null $folder.ProfilePath $true "MissingRegistryEntry" $ComputerName $isSpecial } } } # End block runs once after all processing is complete - end { + end + { # Output all collected profiles $AllProfiles } diff --git a/source/Public/Get-OrphanedProfiles.ps1 b/source/Public/Get-OrphanedProfiles.ps1 index 8426042..023d2a9 100644 --- a/source/Public/Get-OrphanedProfiles.ps1 +++ b/source/Public/Get-OrphanedProfiles.ps1 @@ -1,11 +1,34 @@ -function Get-OrphanedProfiles { +<# +.SYNOPSIS + Retrieves orphaned user profiles from a specified computer. +.DESCRIPTION + The Get-OrphanedProfiles function scans the user profiles on a specified computer and identifies profiles that are orphaned. Orphaned profiles are those that exist either in the filesystem but not in the registry, or vice versa. The function returns a list of only the orphaned profiles. +.PARAMETER ComputerName + The name of the computer from which to retrieve orphaned profiles. Defaults to the local computer. +.PARAMETER ProfileFolderPath + The path to the folder where user profiles are stored. Defaults to "$env:SystemDrive\Users". +.PARAMETER IgnoreSpecial + Switch to ignore special or default profiles (such as system or service accounts) during the orphan detection process. +.EXAMPLE + Get-OrphanedProfiles -ComputerName "Server01" + Retrieves orphaned user profiles from "Server01". +.EXAMPLE + Get-OrphanedProfiles -ProfileFolderPath "D:\UserProfiles" -IgnoreSpecial + Retrieves orphaned profiles from the specified folder while ignoring special or default profiles. +.NOTES + This function relies on the Get-AllUserProfiles function to retrieve profiles from both the filesystem and the registry. + Orphaned profiles are returned as a filtered list where only profiles marked as orphaned are included. +#> + +function Get-OrphanedProfiles +{ [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] - [string]$ProfileFolderPath = "C:\Users", + [string]$ProfileFolderPath = "$env:SystemDrive\Users", [switch]$IgnoreSpecial ) diff --git a/source/Public/Get-ProfilePathFromSID.ps1 b/source/Public/Get-ProfilePathFromSID.ps1 deleted file mode 100644 index 674b66c..0000000 --- a/source/Public/Get-ProfilePathFromSID.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -function Get-ProfilePathFromSID { - param ( - [Microsoft.Win32.RegistryKey]$SidKey - ) - - try { - # Use Get-RegistryValue to retrieve the "ProfileImagePath" - $profileImagePath = Get-RegistryValue -Key $SidKey -ValueName "ProfileImagePath" - - if (-not $profileImagePath) { - Write-Verbose "ProfileImagePath not found for SID '$($SidKey.Name)'." - } - - return $profileImagePath - } catch { - Write-Error "Failed to retrieve ProfileImagePath for SID '$($SidKey.Name)'. Error: $_" - return $null - } -} diff --git a/source/Public/Get-RegistryKeyForSID.ps1 b/source/Public/Get-RegistryKeyForSID.ps1 deleted file mode 100644 index 2ed0833..0000000 --- a/source/Public/Get-RegistryKeyForSID.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -function Get-RegistryKeyForSID { - param ( - [string]$SID, - [Microsoft.Win32.RegistryKey]$ProfileListKey - ) - - try { - # Use the general Open-RegistrySubKey function to get the subkey for the SID - $sidKey = Open-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID - if ($sidKey -eq $null) { - Write-Warning "The SID '$SID' does not exist in the ProfileList registry." - return $null - } - return $sidKey - } catch { - Write-Error "Error accessing registry key for SID '$SID'. Error: $_" - return $null - } -} diff --git a/source/Public/Get-SIDProfileInfo.ps1 b/source/Public/Get-SIDProfileInfo.ps1 index e0d7b81..ac337d7 100644 --- a/source/Public/Get-SIDProfileInfo.ps1 +++ b/source/Public/Get-SIDProfileInfo.ps1 @@ -1,4 +1,22 @@ -function Get-SIDProfileInfo { +<# +.SYNOPSIS + Retrieves profile information from the registry for all SIDs on a specified computer. +.DESCRIPTION + The Get-SIDProfileInfo function queries the ProfileList registry key on the specified computer and retrieves profile information for each Security Identifier (SID). The function returns a list of profiles, including details such as the SID, profile path, and whether the profile exists in the registry. +.PARAMETER ComputerName + The name of the computer from which to retrieve profile information. Defaults to the local computer. +.EXAMPLE + Get-SIDProfileInfo -ComputerName "Server01" + Retrieves profile information for all SIDs stored in the registry on "Server01". +.EXAMPLE + Get-SIDProfileInfo + Retrieves profile information for all SIDs stored in the registry on the local computer. +.NOTES + This function returns a list of objects where each object contains the SID, profile path, and whether the profile exists in the registry. + If a registry subkey for an SID cannot be opened, a warning is written to the output. +#> +function Get-SIDProfileInfo +{ [CmdletBinding()] param ( [string]$ComputerName = $env:COMPUTERNAME @@ -7,16 +25,19 @@ function Get-SIDProfileInfo { $RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName - if ($ProfileListKey -eq $null) { + if ($ProfileListKey -eq $null) + { Write-Error "Failed to open registry path: $RegistryPath on $ComputerName." return } - $ProfileRegistryItems = foreach ($sid in $ProfileListKey.GetSubKeyNames()) { + $ProfileRegistryItems = foreach ($sid in $ProfileListKey.GetSubKeyNames()) + { # Use Open-RegistrySubKey to get the subkey for the SID $subKey = Open-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $sid - if ($subKey -eq $null) { + if ($subKey -eq $null) + { Write-Warning "Registry key for SID '$sid' could not be opened." continue } diff --git a/source/Public/Get-UserFolders.ps1 b/source/Public/Get-UserFolders.ps1 index 397349a..cf81446 100644 --- a/source/Public/Get-UserFolders.ps1 +++ b/source/Public/Get-UserFolders.ps1 @@ -1,8 +1,27 @@ -function Get-UserFolders { +<# +.SYNOPSIS + Retrieves a list of user profile folders from a specified computer. +.DESCRIPTION + The Get-UserFolders function scans the user profile directory on the specified computer and returns a list of folders that represent user profiles. It checks whether the target computer is local or remote and returns information such as the folder name and profile path for each folder. +.PARAMETER ComputerName + The name of the computer from which to retrieve user profile folders. +.PARAMETER ProfileFolderPath + The path to the folder where user profiles are stored. Defaults to "$env:SystemDrive\Users". +.EXAMPLE + Get-UserFolders -ComputerName "Server01" -ProfileFolderPath "D:\UserProfiles" + Retrieves a list of user profile folders from the "D:\UserProfiles" directory on "Server01". +.EXAMPLE + Get-UserFolders -ComputerName $env:COMPUTERNAME + Retrieves a list of user profile folders from the local computer's default user directory. +.NOTES + This function returns an array of objects where each object represents a user profile folder, including the folder name, profile path, and computer name. +#> +function Get-UserFolders +{ [CmdletBinding()] param ( [string]$ComputerName, - [string]$ProfileFolderPath = "C:\Users" + [string]$ProfileFolderPath = "$env:SystemDrive\Users" ) $IsLocal = ($ComputerName -eq $env:COMPUTERNAME) diff --git a/source/Public/Get-UserProfilesFromFolders.ps1 b/source/Public/Get-UserProfilesFromFolders.ps1 index 1ce28e1..ad3b00d 100644 --- a/source/Public/Get-UserProfilesFromFolders.ps1 +++ b/source/Public/Get-UserProfilesFromFolders.ps1 @@ -1,8 +1,26 @@ +<# +.SYNOPSIS + Retrieves user profile folders from a specified computer. +.DESCRIPTION + The Get-UserProfilesFromFolders function scans the user profile directory on the specified computer and returns information about the user profile folders found. This function is useful for identifying the profile folders stored on disk, which may or may not match entries in the registry. +.PARAMETER ComputerName + The name of the computer from which to retrieve user profile folders. Defaults to the local computer. +.PARAMETER ProfileFolderPath + The path to the folder where user profiles are stored. Defaults to "$env:SystemDrive\Users". +.EXAMPLE + Get-UserProfilesFromFolders -ComputerName "Server01" -ProfileFolderPath "D:\UserProfiles" + Retrieves user profile folders from the "D:\UserProfiles" directory on "Server01". +.EXAMPLE + Get-UserProfilesFromFolders + Retrieves user profile folders from the default "$env:SystemDrive\Users" directory on the local computer. +.NOTES + This function returns a list of user profile folders found in the specified directory on the specified computer. +#> function Get-UserProfilesFromFolders { param ( [string]$ComputerName = $env:COMPUTERNAME, - [string]$ProfileFolderPath = "C:\Users" + [string]$ProfileFolderPath = "$env:SystemDrive\Users" ) # Get user folders and return them diff --git a/source/Public/Get-UserProfilesFromRegistry.ps1 b/source/Public/Get-UserProfilesFromRegistry.ps1 index c400d1d..ca0f132 100644 --- a/source/Public/Get-UserProfilesFromRegistry.ps1 +++ b/source/Public/Get-UserProfilesFromRegistry.ps1 @@ -1,3 +1,19 @@ +<# +.SYNOPSIS + Retrieves user profiles from the registry of a specified computer. +.DESCRIPTION + The Get-UserProfilesFromRegistry function queries the ProfileList registry key on the specified computer and returns information about the user profiles found in the registry. This includes details such as the SID and profile path. +.PARAMETER ComputerName + The name of the computer from which to retrieve user profiles. Defaults to the local computer. +.EXAMPLE + Get-UserProfilesFromRegistry -ComputerName "Server01" + Retrieves the user profiles from the registry on "Server01". +.EXAMPLE + Get-UserProfilesFromRegistry + Retrieves the user profiles from the local computer's registry. +.NOTES + This function returns a list of user profiles stored in the registry, including their SIDs and associated profile paths. +#> function Get-UserProfilesFromRegistry { param ( @@ -8,3 +24,4 @@ function Get-UserProfilesFromRegistry $RegistryProfiles = Get-SIDProfileInfo -ComputerName $ComputerName return $RegistryProfiles } + diff --git a/source/Public/New-UserProfileObject.ps1 b/source/Public/New-UserProfileObject.ps1 deleted file mode 100644 index 2d697a0..0000000 --- a/source/Public/New-UserProfileObject.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -function New-UserProfileObject { - param ( - [string]$SID, - [string]$ProfilePath, - [bool]$IsOrphaned, - [string]$OrphanReason, - [string]$ComputerName, - [bool]$IsSpecial - ) - - return [UserProfile]::new( - $SID, - $ProfilePath, - $IsOrphaned, - $OrphanReason, - $ComputerName, - $IsSpecial - ) -} diff --git a/source/Public/Remove-OrphanedProfiles.ps1 b/source/Public/Remove-OrphanedProfiles.ps1 index c61ba91..a4f8d96 100644 --- a/source/Public/Remove-OrphanedProfiles.ps1 +++ b/source/Public/Remove-OrphanedProfiles.ps1 @@ -1,11 +1,34 @@ -function Remove-OrphanedProfiles { +<# +.SYNOPSIS + Removes orphaned user profiles from a specified computer. +.DESCRIPTION + The Remove-OrphanedProfiles function identifies and removes orphaned profiles from the specified computer. Orphaned profiles are those that exist in the file system but not in the registry, or vice versa. The function can also optionally ignore special or default profiles. +.PARAMETER ComputerName + The name of the computer from which orphaned profiles will be removed. This is a required parameter. +.PARAMETER ProfileFolderPath + The path to the folder containing user profiles. Defaults to "$env:SystemDrive\Users". +.PARAMETER IgnoreSpecial + Switch to ignore special or default profiles during the removal process. +.EXAMPLE + Remove-OrphanedProfiles -ComputerName "Server01" -ProfileFolderPath "C:\Users" -IgnoreSpecial + Removes orphaned profiles from "Server01", excluding special or default profiles. +.EXAMPLE + Remove-OrphanedProfiles -ComputerName "Server01" + Removes orphaned profiles from "Server01" using the default profile folder path "$env:SystemDrive\Users". +.NOTES + This function supports 'ShouldProcess', allowing the use of -WhatIf or -Confirm to simulate the deletion process. + The function first collects all orphaned profiles, identifies the SIDs associated with them, and then removes the corresponding registry entries. +#> + +function Remove-OrphanedProfiles +{ [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $true)] [string]$ComputerName, [Parameter(Mandatory = $false)] - [string]$ProfileFolderPath = "C:\Users", + [string]$ProfileFolderPath = "$env:SystemDrive\Users", [switch]$IgnoreSpecial ) @@ -13,7 +36,8 @@ function Remove-OrphanedProfiles { # Step 1: Get the list of orphaned profiles $orphanedProfiles = Get-OrphanedProfiles-ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial - if (-not $orphanedProfiles) { + if (-not $orphanedProfiles) + { Write-Verbose "No orphaned profiles found on $ComputerName." return } @@ -21,7 +45,8 @@ function Remove-OrphanedProfiles { # Step 2: Extract the SIDs of orphaned profiles that exist in the registry $orphanedSIDs = $orphanedProfiles | Where-Object { $_.SID } | Select-Object -ExpandProperty SID - if (-not $orphanedSIDs) { + if (-not $orphanedSIDs) + { Write-Verbose "No orphaned profiles with valid SIDs found for removal on $ComputerName." return } diff --git a/source/Public/Remove-ProfilesForSIDs.ps1 b/source/Public/Remove-ProfilesForSIDs.ps1 index 6e8faea..a812fde 100644 --- a/source/Public/Remove-ProfilesForSIDs.ps1 +++ b/source/Public/Remove-ProfilesForSIDs.ps1 @@ -1,9 +1,29 @@ -function Remove-ProfilesForSIDs { + +<# +.SYNOPSIS + Orchestrates the deletion process for multiple profiles by SID. +.DESCRIPTION + The Remove-ProfilesForSIDs function removes profiles for multiple Security Identifiers (SIDs) from the ProfileList in the Windows registry. It loops through each SID provided, attempts to delete the associated profile, and returns the results of each deletion. + If a profile cannot be found in the registry, the function will return a result indicating that the profile was not found. +.PARAMETER SIDs + An array of Security Identifiers (SIDs) for which the profile registry keys will be deleted. +.PARAMETER ComputerName + The name of the computer where the profiles reside. By default, this is the local computer. +.EXAMPLE + Remove-ProfilesForSIDs -SIDs "S-1-5-21-123456789-1001", "S-1-5-21-123456789-1002" -ComputerName "Server01" + Deletes the profiles associated with the specified SIDs from the registry on "Server01" and returns the results of each deletion. +.NOTES + This function supports 'ShouldProcess', allowing the use of -WhatIf or -Confirm to simulate the deletion process. + Each profile deletion is handled individually, with errors caught and returned in the final result. +#> + +function Remove-ProfilesForSIDs +{ #Orchestrates the deletion process for multiple SIDs. [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $true)] - [string[]]$SIDs, # Accept multiple SIDs as an array + [string[]]$SIDs, # Accept multiple SIDs as an array [Parameter(Mandatory = $false)] [string]$ComputerName = $env:COMPUTERNAME # Default to local computer @@ -13,7 +33,8 @@ function Remove-ProfilesForSIDs { $RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName - if ($ProfileListKey -eq $null) { + if ($ProfileListKey -eq $null) + { Write-Error "Failed to open ProfileList registry path on $ComputerName." return } @@ -21,12 +42,15 @@ function Remove-ProfilesForSIDs { $deletionResults = @() # Loop through each SID and process deletion - foreach ($sid in $SIDs) { - try { + foreach ($sid in $SIDs) + { + try + { # Get profile information for the SID $sidProfileInfo = Get-SIDProfileInfo -SID $sid -ProfileListKey $ProfileListKey - if (-not $sidProfileInfo.ExistsInRegistry) { + if (-not $sidProfileInfo.ExistsInRegistry) + { $deletionResults += [ProfileDeletionResult]::new( $sid, $null, @@ -39,12 +63,14 @@ function Remove-ProfilesForSIDs { # Process the deletion of the profile for the SID $deletionResult = Remove-SIDProfile -SID $sid ` - -ProfileListKey $ProfileListKey ` - -ComputerName $ComputerName ` - -ProfilePath $sidProfileInfo.ProfilePath + -ProfileListKey $ProfileListKey ` + -ComputerName $ComputerName ` + -ProfilePath $sidProfileInfo.ProfilePath $deletionResults += $deletionResult - } catch { + } + catch + { Write-Error "An error occurred while processing SID '$sid'. $_" # Add a deletion result indicating failure due to error diff --git a/source/Public/Remove-RegistryKeyForSID.ps1 b/source/Public/Remove-RegistryKeyForSID.ps1 deleted file mode 100644 index 84b6b8f..0000000 --- a/source/Public/Remove-RegistryKeyForSID.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -function Remove-RegistryKeyForSID { - #Deletes a single registry key for a SID. - [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')] - param ( - [Parameter(Mandatory = $true)] - [string]$SID, - - [Parameter(Mandatory = $true)] - [Microsoft.Win32.RegistryKey]$ProfileListKey, - - [Parameter(Mandatory = $true)] - [string]$ComputerName = $env:COMPUTERNAME - ) - - try { - # Use the general Remove-RegistrySubKey function to delete the SID's subkey - return Remove-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID -ComputerName $ComputerName - } catch { - Write-Error "Failed to remove the profile registry key for SID '$SID' on $ComputerName. Error: $_" - return $false - } -} diff --git a/source/Public/Remove-SIDProfile.ps1 b/source/Public/Remove-SIDProfile.ps1 deleted file mode 100644 index cc46898..0000000 --- a/source/Public/Remove-SIDProfile.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -function Remove-SIDProfile { - #Coordinates the registry key deletion and provides a result for a single SID. - [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')] - param ( - [string]$SID, - [Microsoft.Win32.RegistryKey]$ProfileListKey, - [string]$ComputerName, - [string]$ProfilePath - ) - - # Attempt to remove the registry key - $deletionSuccess = Remove-RegistryKeyForSID -SID $SID -ProfileListKey $ProfileListKey -ComputerName $ComputerName - - if ($deletionSuccess) { - return [ProfileDeletionResult]::new( - $SID, - $ProfilePath, - $true, - "Profile registry key for SID '$SID' successfully deleted.", - $ComputerName - ) - } else { - return [ProfileDeletionResult]::new( - $SID, - $ProfilePath, - $false, - "Failed to delete the profile registry key for SID '$SID'.", - $ComputerName - ) - } -} diff --git a/source/Public/Test-FolderExists.ps1 b/source/Public/Test-FolderExists.ps1 deleted file mode 100644 index 2d23252..0000000 --- a/source/Public/Test-FolderExists.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -function Test-FolderExists { - param ( - [string]$ProfilePath, - [string]$ComputerName - ) - - $IsLocal = $ComputerName -eq $env:COMPUTERNAME - $pathToCheck = Get-DirectoryPath -BasePath $ProfilePath -ComputerName $ComputerName -IsLocal $IsLocal - return Test-Path $pathToCheck -} diff --git a/source/Public/Test-OrphanedProfile.ps1 b/source/Public/Test-OrphanedProfile.ps1 deleted file mode 100644 index db4d4c7..0000000 --- a/source/Public/Test-OrphanedProfile.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -function Test-OrphanedProfile { - param ( - [string]$SID, - [string]$ProfilePath, - [bool]$FolderExists, - [bool]$IgnoreSpecial, - [bool]$IsSpecial, - [string]$ComputerName - ) - - if (-not $ProfilePath) { - return New-UserProfileObject $SID "(null)" $true "MissingProfileImagePath" $ComputerName $IsSpecial - } - elseif (-not $FolderExists) { - return New-UserProfileObject $SID $ProfilePath $true "MissingFolder" $ComputerName $IsSpecial - } - else { - return New-UserProfileObject $SID $ProfilePath $false $null $ComputerName $IsSpecial - } -} diff --git a/source/Public/Test-SpecialAccount.ps1 b/source/Public/Test-SpecialAccount.ps1 deleted file mode 100644 index e669979..0000000 --- a/source/Public/Test-SpecialAccount.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -function Test-SpecialAccount { - param ( - [string]$FolderName, - [string]$SID, - [string]$ProfilePath - ) - - # List of default or special accounts to ignore - $IgnoredAccounts = @( - "defaultuser0", "DefaultAppPool", "servcm12", "Public", "PBIEgwService", "Default", - "All Users", "win2kpro" - ) - $IgnoredSIDs = @( - "S-1-5-18", # Local System - "S-1-5-19", # Local Service - "S-1-5-20" # Network Service - ) - $IgnoredPaths = @( - "C:\WINDOWS\system32\config\systemprofile", # System profile - "C:\WINDOWS\ServiceProfiles\LocalService", # Local service profile - "C:\WINDOWS\ServiceProfiles\NetworkService" # Network service profile - ) - - # Check if the account is special based on the folder name, SID, or profile path - return ($IgnoredAccounts -contains $FolderName) -or ($IgnoredSIDs -contains $SID) -or ($IgnoredPaths -contains $ProfilePath) -} From 5840407f12613395687c2c69f0689268d9ad9349 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 11 Sep 2024 00:00:05 -0700 Subject: [PATCH 02/11] add `Test-FolderExists` Unit Tests --- source/Private/Test-FolderExists.ps1 | 58 ++++- .../Unit/Private/Test-FolderExists.tests.ps1 | 225 ++++++++++++++++++ 2 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 tests/Unit/Private/Test-FolderExists.tests.ps1 diff --git a/source/Private/Test-FolderExists.ps1 b/source/Private/Test-FolderExists.ps1 index 7dee156..1547a7e 100644 --- a/source/Private/Test-FolderExists.ps1 +++ b/source/Private/Test-FolderExists.ps1 @@ -3,23 +3,69 @@ Checks if a profile folder exists on a specified computer. .DESCRIPTION The Test-FolderExists function determines whether a given profile folder exists on the specified computer by testing the path. + If the profile path or computer name is not provided, the function will default to using the local computer. + In the event of any errors (e.g., invalid paths or inaccessible directories), the function returns $false and logs the error. + .PARAMETER ProfilePath - The file path of the profile folder to check. + The file path of the profile folder to check. This parameter is required. If it is null or empty, the function will return $false. .PARAMETER ComputerName - The name of the computer where the profile folder is located. + The name of the computer where the profile folder is located. If not provided, the local computer is used by default. +.OUTPUTS + [bool] + Returns $true if the folder exists at the specified path, and $false if it does not exist, or if an error occurs during execution. + .EXAMPLE Test-FolderExists -ProfilePath "C:\Users\John" -ComputerName "Server01" Checks if the folder "C:\Users\John" exists on "Server01". + +.EXAMPLE + Test-FolderExists -ProfilePath "C:\Users\Public" + Checks if the folder "C:\Users\Public" exists on the local computer (since ComputerName is not specified). + +.EXAMPLE + Test-FolderExists -ProfilePath "C:\InvalidPath" -ComputerName "Server01" + Returns $false if the specified folder does not exist or if an error occurs while accessing the path. + +.NOTES + The function includes error handling to catch and log any exceptions. In case of an error, the function returns $false. #> function Test-FolderExists { + [outputType([bool])] param ( [string]$ProfilePath, - [string]$ComputerName + [string]$ComputerName = $env:COMPUTERNAME ) - $IsLocal = $ComputerName -eq $env:COMPUTERNAME - $pathToCheck = Get-DirectoryPath -BasePath $ProfilePath -ComputerName $ComputerName -IsLocal $IsLocal - return Test-Path $pathToCheck + # Check for null or empty ProfilePath + if (-not $ProfilePath) + { + Write-Warning "ProfilePath is null or empty." + return $false + } + + # Check for null or empty ComputerName and default to the local computer if it's null + if (-not $ComputerName) + { + Write-Warning "ComputerName is null or empty. Defaulting to the local computer." + $ComputerName = $env:COMPUTERNAME + } + + try + { + # Determine if the computer is local or remote + $IsLocal = $ComputerName -eq $env:COMPUTERNAME + + # Get the directory path to check + $pathToCheck = Get-DirectoryPath -BasePath $ProfilePath -ComputerName $ComputerName -IsLocal $IsLocal + + # Return whether the path exists + return Test-Path $pathToCheck + } + catch + { + Write-Error "An error occurred: $_" + return $false + } } diff --git a/tests/Unit/Private/Test-FolderExists.tests.ps1 b/tests/Unit/Private/Test-FolderExists.tests.ps1 new file mode 100644 index 0000000..f50708a --- /dev/null +++ b/tests/Unit/Private/Test-FolderExists.tests.ps1 @@ -0,0 +1,225 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe "Test-FolderExists" -Tag "Private" { + + Context "Folder Existence Tests" { + + Context "When Testing Local Folders" { + + It "Should return `$true for an existing local folder" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath { return "C:\Users" } + Mock Test-Path { return $true } + + $profilePath = "C:\Users\Public" + $computerName = $env:COMPUTERNAME + + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + + $result | Should -BeTrue + Assert-MockCalled Get-DirectoryPath -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + } + } + + It "Should return `$false for a non-existent local folder" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath { return "C:\InvalidFolder" } + Mock Test-Path { return $false } + + $profilePath = "C:\InvalidFolder" + $computerName = $env:COMPUTERNAME + + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + + $result | Should -BeFalse + Assert-MockCalled Get-DirectoryPath -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + } + } + } + + Context "When Testing Remote Folders" { + + It "Should return `$true for an existing remote folder" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath { return "\\RemotePC\Users" } + Mock Test-Path { return $true } + + $profilePath = "\\RemotePC\Users\John" + $computerName = "RemotePC" + + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + + $result | Should -BeTrue + Assert-MockCalled Get-DirectoryPath -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + } + } + + It "Should return `$false for a non-existent remote folder" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath { return "\\RemotePC\InvalidFolder" } + Mock Test-Path { return $false } + + $profilePath = "\\RemotePC\InvalidFolder" + $computerName = "RemotePC" + + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + + $result | Should -BeFalse + Assert-MockCalled Get-DirectoryPath -Exactly 1 -Scope It + Assert-MockCalled Test-Path -Exactly 1 -Scope It + } + } + } + } + + Context "Computer Identification Tests" { + + It "Should correctly identify the local computer" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath { return "C:\Users" } + Mock Test-Path { return $true } + + $profilePath = "C:\Users\John" + $computerName = $env:COMPUTERNAME + + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + + Assert-MockCalled Get-DirectoryPath -ParameterFilter { $IsLocal -eq $true } -Exactly 1 + } + } + + It "Should correctly identify a remote computer" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath -ParameterFilter { $IsLocal -eq $false } { return "\\RemotePC\Users" } + Mock Test-Path { return $true } + + $profilePath = "\\RemotePC\Users\John" + $computerName = "RemotePC" + + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + + Assert-MockCalled Get-DirectoryPath -ParameterFilter { $IsLocal -eq $false } -Exactly 1 + } + } + } + + Context "Input Validation Tests" { + + It "Should return `$false and log a warning when ProfilePath is `$null" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath + Mock Test-Path + + $profilePath = $null + $computerName = $env:COMPUTERNAME + + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + + $result | Should -BeFalse + Assert-MockCalled Get-DirectoryPath -Exactly 0 + Assert-MockCalled Test-Path -Exactly 0 + } + } + + It "Should return `$false and log a warning when ProfilePath is an empty string" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath + Mock Test-Path + + $profilePath = "" + $computerName = $env:COMPUTERNAME + + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + + $result | Should -BeFalse + Assert-MockCalled Get-DirectoryPath -Exactly 0 + Assert-MockCalled Test-Path -Exactly 0 + } + } + + It "Should default to the local computer when ComputerName is `$null" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath { return "C:\Users" } + Mock Test-Path { return $true } + + $profilePath = "C:\Users\John" + $computerName = $null + + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + + $result | Should -BeTrue + Assert-MockCalled Get-DirectoryPath -ParameterFilter { $ComputerName -eq $env:COMPUTERNAME } -Exactly 1 + } + } + + It "Should default to the local computer when ComputerName is an empty string" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath { return "C:\Users" } + Mock Test-Path { return $true } + + $profilePath = "C:\Users\John" + $computerName = "" + + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + + $result | Should -BeTrue + Assert-MockCalled Get-DirectoryPath -ParameterFilter { $ComputerName -eq $env:COMPUTERNAME } -Exactly 1 + } + } + } + + Context "Error Handling Tests" { + + It "Should return `$false if Get-DirectoryPath throws an error" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath { throw "Path resolution failed" } + + $profilePath = "C:\Users\John" + $computerName = $env:COMPUTERNAME + + # Act and Assert + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + $result | Should -BeFalse + + Assert-MockCalled Get-DirectoryPath -Exactly 1 + } + } + + It "Should return `$false if Test-Path throws an error" { + InModuleScope -ScriptBlock { + Mock Get-DirectoryPath { return "C:\Users" } + Mock Test-Path { throw "Test-Path failed" } + + $profilePath = "C:\Users\John" + $computerName = $env:COMPUTERNAME + + # Act and Assert + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + $result | Should -BeFalse + + Assert-MockCalled Get-DirectoryPath -Exactly 1 + Assert-MockCalled Test-Path -Exactly 1 + } + } + } +} From 7da2233cadcd88e8a1e1185e424f31ce4dd3c058 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 11 Sep 2024 00:22:54 -0700 Subject: [PATCH 03/11] add `Test-OrphanedProfile` unit tests --- source/Classes/UserProfile.ps1 | 8 +- source/Private/New-UserProfileObject.ps1 | 2 +- source/Private/Test-OrphanedProfile.ps1 | 2 +- .../Private/Test-OrphanedProfile.tests.ps1 | 159 ++++++++++++++++++ 4 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 tests/Unit/Private/Test-OrphanedProfile.tests.ps1 diff --git a/source/Classes/UserProfile.ps1 b/source/Classes/UserProfile.ps1 index cd231f8..5d981ca 100644 --- a/source/Classes/UserProfile.ps1 +++ b/source/Classes/UserProfile.ps1 @@ -1,13 +1,15 @@ -class UserProfile { +class UserProfile +{ [string]$SID [string]$ProfilePath [bool]$IsOrphaned - [string]$OrphanReason + [string]$OrphanReason = $null [string]$ComputerName [bool]$IsSpecial # Constructor to initialize the properties - UserProfile([string]$sid, [string]$profilePath, [bool]$isOrphaned, [string]$orphanReason, [string]$computerName, [bool]$isSpecial) { + UserProfile([string]$sid, [string]$profilePath, [bool]$isOrphaned, [string]$orphanReason, [string]$computerName, [bool]$isSpecial) + { $this.SID = $sid $this.ProfilePath = $profilePath $this.IsOrphaned = $isOrphaned diff --git a/source/Private/New-UserProfileObject.ps1 b/source/Private/New-UserProfileObject.ps1 index d6f5f5d..f1574ee 100644 --- a/source/Private/New-UserProfileObject.ps1 +++ b/source/Private/New-UserProfileObject.ps1 @@ -28,7 +28,7 @@ function New-UserProfileObject [string]$SID, [string]$ProfilePath, [bool]$IsOrphaned, - [string]$OrphanReason, + [string]$OrphanReason = $null, [string]$ComputerName, [bool]$IsSpecial ) diff --git a/source/Private/Test-OrphanedProfile.ps1 b/source/Private/Test-OrphanedProfile.ps1 index 5228ef5..62b3317 100644 --- a/source/Private/Test-OrphanedProfile.ps1 +++ b/source/Private/Test-OrphanedProfile.ps1 @@ -33,7 +33,7 @@ function Test-OrphanedProfile if (-not $ProfilePath) { - return New-UserProfileObject $SID "(null)" $true "MissingProfileImagePath" $ComputerName $IsSpecial + return New-UserProfileObject $SID $null $true "MissingProfileImagePath" $ComputerName $IsSpecial } elseif (-not $FolderExists) { diff --git a/tests/Unit/Private/Test-OrphanedProfile.tests.ps1 b/tests/Unit/Private/Test-OrphanedProfile.tests.ps1 new file mode 100644 index 0000000..b737283 --- /dev/null +++ b/tests/Unit/Private/Test-OrphanedProfile.tests.ps1 @@ -0,0 +1,159 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +# Import the module if necessary +# Import-Module YourModule + +Describe "Test-OrphanedProfile" -Tag 'Private' { + + Context "When ProfilePath is null" { + It "Should return a profile object with IsOrphaned set to true and reason 'MissingProfileImagePath'" { + + InModuleScope -ScriptBlock { + + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profilePath = $null + $folderExists = $true + $ignoreSpecial = $false + $isSpecial = $false + $computerName = "Server01" + + # Act + $result = Test-OrphanedProfile -SID $sid -ProfilePath $profilePath -FolderExists $folderExists ` + -IgnoreSpecial $ignoreSpecial -IsSpecial $isSpecial -ComputerName $computerName + + # Assert + $result.SID | Should -Be $sid + $result.ProfilePath | Should -BeNullOrEmpty + $result.IsOrphaned | Should -Be $true + $result.OrphanReason | Should -Be "MissingProfileImagePath" + $result.ComputerName | Should -Be $computerName + + } + } + } + + Context "When Profile folder does not exist" { + It "Should return a profile object with IsOrphaned set to true and reason 'MissingFolder'" { + + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + $folderExists = $false + $ignoreSpecial = $false + $isSpecial = $false + $computerName = "Server01" + + # Act + $result = Test-OrphanedProfile -SID $sid -ProfilePath $profilePath -FolderExists $folderExists ` + -IgnoreSpecial $ignoreSpecial -IsSpecial $isSpecial -ComputerName $computerName + + # Assert + $result.SID | Should -Be $sid + $result.ProfilePath | Should -Be $profilePath + $result.IsOrphaned | Should -Be $true + $result.OrphanReason | Should -Be "MissingFolder" + $result.ComputerName | Should -Be $computerName + } + } + } + + Context "When Profile folder exists and profile is not orphaned" { + It "Should return a profile object with IsOrphaned set to false" { + + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + $folderExists = $true + $ignoreSpecial = $false + $isSpecial = $false + $computerName = "Server01" + + # Act + $result = Test-OrphanedProfile -SID $sid -ProfilePath $profilePath -FolderExists $folderExists ` + -IgnoreSpecial $ignoreSpecial -IsSpecial $isSpecial -ComputerName $computerName + + # Assert + $result.SID | Should -Be $sid + $result.ProfilePath | Should -Be $profilePath + $result.IsOrphaned | Should -Be $false + $result.OrphanReason | Should -BeNullOrEmpty + $result.ComputerName | Should -Be $computerName + } + } + } + + Context "When Profile is a special account and IgnoreSpecial is set" { + It "Should return a profile object with IsOrphaned set to false" { + + InModuleScope -ScriptBlock { + + # Arrange + $sid = "S-1-5-18" # Local System SID + $profilePath = "C:\Users\SystemProfile" + $folderExists = $true + $ignoreSpecial = $true + $isSpecial = $true + $computerName = "Server01" + + # Act + $result = Test-OrphanedProfile -SID $sid -ProfilePath $profilePath -FolderExists $folderExists ` + -IgnoreSpecial $ignoreSpecial -IsSpecial $isSpecial -ComputerName $computerName + + # Assert + $result.SID | Should -Be $sid + $result.ProfilePath | Should -Be $profilePath + $result.IsOrphaned | Should -Be $false + $result.OrphanReason | Should -BeNullOrEmpty + $result.ComputerName | Should -Be $computerName + + } + } + } + + Context "When Profile is a special account and IgnoreSpecial is not set" { + It "Should return a profile object with IsOrphaned set to false but IsSpecial should be true" { + + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-18" # Local System SID + $profilePath = "C:\Users\SystemProfile" + $folderExists = $true + $ignoreSpecial = $false + $isSpecial = $true + $computerName = "Server01" + + # Act + $result = Test-OrphanedProfile -SID $sid -ProfilePath $profilePath -FolderExists $folderExists ` + -IgnoreSpecial $ignoreSpecial -IsSpecial $isSpecial -ComputerName $computerName + + # Assert + $result.SID | Should -Be $sid + $result.ProfilePath | Should -Be $profilePath + $result.IsOrphaned | Should -Be $false + $result.OrphanReason | Should -BeNullOrEmpty + $result.IsSpecial | Should -Be $true + $result.ComputerName | Should -Be $computerName + } + } + } +} From b20958d8858fd181736452f1cca8d7aa6a888398 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 11 Sep 2024 00:27:43 -0700 Subject: [PATCH 04/11] add `Test-SpecialAccount` unit tests --- .../Private/Test-SpecialAccount.tests.ps1 | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 tests/Unit/Private/Test-SpecialAccount.tests.ps1 diff --git a/tests/Unit/Private/Test-SpecialAccount.tests.ps1 b/tests/Unit/Private/Test-SpecialAccount.tests.ps1 new file mode 100644 index 0000000..c25db85 --- /dev/null +++ b/tests/Unit/Private/Test-SpecialAccount.tests.ps1 @@ -0,0 +1,153 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe "Test-SpecialAccount" -Tag 'Private' { + + Context "When Testing Special Folder Names" { + + It "Should return $true for a special folder name" { + InModuleScope -ScriptBlock { + # Arrange + $folderName = "defaultuser0" + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + + # Act + $result = Test-SpecialAccount -FolderName $folderName -SID $sid -ProfilePath $profilePath + + # Assert + $result | Should -BeTrue + } + } + + It "Should return $false for a non-special folder name" { + InModuleScope -ScriptBlock { + # Arrange + $folderName = "John" + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + + # Act + $result = Test-SpecialAccount -FolderName $folderName -SID $sid -ProfilePath $profilePath + + # Assert + $result | Should -BeFalse + } + } + } + + Context "When Testing Special SIDs" { + + It "Should return $true for a special SID" { + InModuleScope -ScriptBlock { + # Arrange + $folderName = "John" + $sid = "S-1-5-18" # Local System SID + $profilePath = "C:\Users\John" + + # Act + $result = Test-SpecialAccount -FolderName $folderName -SID $sid -ProfilePath $profilePath + + # Assert + $result | Should -BeTrue + } + } + + It "Should return $false for a non-special SID" { + InModuleScope -ScriptBlock { + # Arrange + $folderName = "John" + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + + # Act + $result = Test-SpecialAccount -FolderName $folderName -SID $sid -ProfilePath $profilePath + + # Assert + $result | Should -BeFalse + } + } + } + + Context "When Testing Special Profile Paths" { + + It "Should return $true for a special profile path" { + InModuleScope -ScriptBlock { + # Arrange + $folderName = "John" + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\WINDOWS\system32\config\systemprofile" + + # Act + $result = Test-SpecialAccount -FolderName $folderName -SID $sid -ProfilePath $profilePath + + # Assert + $result | Should -BeTrue + } + } + + It "Should return $false for a non-special profile path" { + InModuleScope -ScriptBlock { + # Arrange + $folderName = "John" + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + + # Act + $result = Test-SpecialAccount -FolderName $folderName -SID $sid -ProfilePath $profilePath + + # Assert + $result | Should -BeFalse + } + } + } + + Context "When Testing Combined Conditions" { + + It "Should return $true if any condition (folder name, SID, or profile path) is special" { + InModuleScope -ScriptBlock { + # Arrange + $folderName = "John" + $sid = "S-1-5-19" # Local Service SID (special) + $profilePath = "C:\Users\John" + + # Act + $result = Test-SpecialAccount -FolderName $folderName -SID $sid -ProfilePath $profilePath + + # Assert + $result | Should -BeTrue + } + } + + It "Should return $false if none of the conditions are special" { + InModuleScope -ScriptBlock { + # Arrange + $folderName = "John" + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + + # Act + $result = Test-SpecialAccount -FolderName $folderName -SID $sid -ProfilePath $profilePath + + # Assert + $result | Should -BeFalse + } + } + } +} From b4cc9febc472a2a88cde960351fc29d0d241182d Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 11 Sep 2024 00:36:24 -0700 Subject: [PATCH 05/11] add `Get-ProfilePathFromSID` unit tests --- .../Private/Get-ProfilePathFromSID.tests.ps1 | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/Unit/Private/Get-ProfilePathFromSID.tests.ps1 diff --git a/tests/Unit/Private/Get-ProfilePathFromSID.tests.ps1 b/tests/Unit/Private/Get-ProfilePathFromSID.tests.ps1 new file mode 100644 index 0000000..3e22443 --- /dev/null +++ b/tests/Unit/Private/Get-ProfilePathFromSID.tests.ps1 @@ -0,0 +1,88 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe "Get-ProfilePathFromSID" -Tag 'Private' { + + Context "When the ProfileImagePath exists" { + + It "Should return the correct ProfileImagePath" { + InModuleScope -ScriptBlock { + # Arrange + $sidKey = New-MockObject -type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = 'S-1-5-21-123456789-1001' } + + Mock Get-RegistryValue { + return "C:\Users\John" + } + + # Act + $result = Get-ProfilePathFromSID -SidKey $sidKey + + # Assert + $result | Should -Be "C:\Users\John" + Assert-MockCalled Get-RegistryValue -Exactly 1 -Scope It + } + } + } + + Context "When the ProfileImagePath does not exist" { + + It "Should return $null and write a verbose message" { + InModuleScope -ScriptBlock { + # Arrange + $sidKey = New-MockObject -type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = 'S-1-5-21-123456789-1001' } + + Mock Get-RegistryValue { + return $null + } + + Mock Write-Verbose + + # Act + $result = Get-ProfilePathFromSID -SidKey $sidKey + + # Assert + $result | Should -Be $null + Assert-MockCalled Get-RegistryValue -Exactly 1 -Scope It + Assert-MockCalled Write-Verbose -Exactly 1 -Scope It + } + } + } + + Context "When an error occurs while retrieving ProfileImagePath" { + + It "Should return $null and write an error message" { + InModuleScope -ScriptBlock { + # Arrange + $sidKey = New-MockObject -type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = 'S-1-5-21-123456789-1001' } + + Mock Get-RegistryValue { throw "Registry access error" } + + Mock Write-Error + + # Act + $result = Get-ProfilePathFromSID -SidKey $sidKey + + # Assert + $result | Should -Be $null + Assert-MockCalled Get-RegistryValue -Exactly 1 -Scope It + Assert-MockCalled Write-Error -Exactly 1 -Scope It + } + } + } +} From 963b8da1f90091e79ebfb7004d839b5cbdbafcc7 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 11 Sep 2024 00:46:25 -0700 Subject: [PATCH 06/11] add `Get-RegistryKeyForSID` unit tests --- .../Private/Get-RegistryKeyForSID.tests.ps1 | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/Unit/Private/Get-RegistryKeyForSID.tests.ps1 diff --git a/tests/Unit/Private/Get-RegistryKeyForSID.tests.ps1 b/tests/Unit/Private/Get-RegistryKeyForSID.tests.ps1 new file mode 100644 index 0000000..91ff475 --- /dev/null +++ b/tests/Unit/Private/Get-RegistryKeyForSID.tests.ps1 @@ -0,0 +1,92 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe "Get-RegistryKeyForSID" -Tag 'Private' { + + Context "When the SID registry key exists" { + + It "Should return the correct SID registry key" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profileListKey = New-MockObject -type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + $sidKey = New-MockObject -type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = $sid } + + Mock Open-RegistrySubKey { + return $sidKey + } + + # Act + $result = Get-RegistryKeyForSID -SID $sid -ProfileListKey $profileListKey + + # Assert + $result | Should -Be $sidKey + Assert-MockCalled Open-RegistrySubKey -Exactly 1 -Scope It + } + } + } + + Context "When the SID registry key does not exist" { + + It "Should return $null and write a warning" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profileListKey = New-MockObject -type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + + Mock Open-RegistrySubKey { + return $null + } + + Mock Write-Warning + + # Act + $result = Get-RegistryKeyForSID -SID $sid -ProfileListKey $profileListKey + + # Assert + $result | Should -Be $null + Assert-MockCalled Open-RegistrySubKey -Exactly 1 -Scope It + Assert-MockCalled Write-Warning -Exactly 1 -Scope It + } + } + } + + Context "When an error occurs while accessing the SID registry key" { + + It "Should return $null and write an error" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profileListKey = New-MockObject -type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + + Mock Open-RegistrySubKey { throw "Registry access error" } + + Mock Write-Error + + # Act + $result = Get-RegistryKeyForSID -SID $sid -ProfileListKey $profileListKey + + # Assert + $result | Should -Be $null + Assert-MockCalled Open-RegistrySubKey -Exactly 1 -Scope It + Assert-MockCalled Write-Error -Exactly 1 -Scope It + } + } + } +} From b94d2cafee8ac0c3f77d02f44f2c88ee58f88a22 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 11 Sep 2024 00:52:52 -0700 Subject: [PATCH 07/11] add 'New-UserProfileObject; unit tests --- source/Private/New-UserProfileObject.ps1 | 1 + .../Private/New-UserProfileObject.tests.ps1 | 94 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 tests/Unit/Private/New-UserProfileObject.tests.ps1 diff --git a/source/Private/New-UserProfileObject.ps1 b/source/Private/New-UserProfileObject.ps1 index f1574ee..b74b237 100644 --- a/source/Private/New-UserProfileObject.ps1 +++ b/source/Private/New-UserProfileObject.ps1 @@ -24,6 +24,7 @@ function New-UserProfileObject { + [outputType([UserProfile])] param ( [string]$SID, [string]$ProfilePath, diff --git a/tests/Unit/Private/New-UserProfileObject.tests.ps1 b/tests/Unit/Private/New-UserProfileObject.tests.ps1 new file mode 100644 index 0000000..717ad87 --- /dev/null +++ b/tests/Unit/Private/New-UserProfileObject.tests.ps1 @@ -0,0 +1,94 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe "New-UserProfileObject" -Tag 'Private' { + + Context "When creating a new UserProfile object" { + + It "Should return a valid UserProfile object with all properties set" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + $isOrphaned = $true + $orphanReason = "MissingRegistryEntry" + $computerName = "Server01" + $isSpecial = $false + + # Act + $result = New-UserProfileObject -SID $sid -ProfilePath $profilePath -IsOrphaned $isOrphaned -OrphanReason $orphanReason -ComputerName $computerName -IsSpecial $isSpecial + + # Assert + $result.GetType().name | Should -Be 'UserProfile' + $result.SID | Should -Be $sid + $result.ProfilePath | Should -Be $profilePath + $result.IsOrphaned | Should -Be $isOrphaned + $result.OrphanReason | Should -Be $orphanReason + $result.ComputerName | Should -Be $computerName + $result.IsSpecial | Should -Be $isSpecial + } + } + + It "Should return a UserProfile object with a null OrphanReason if not provided" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + $isOrphaned = $false + $computerName = "Server01" + $isSpecial = $false + + # Act + $result = New-UserProfileObject -SID $sid -ProfilePath $profilePath -IsOrphaned $isOrphaned -ComputerName $computerName -IsSpecial $isSpecial + + # Assert + $result.GetType().name | Should -Be 'UserProfile' + $result.SID | Should -Be $sid + $result.ProfilePath | Should -Be $profilePath + $result.IsOrphaned | Should -Be $isOrphaned + $result.OrphanReason | Should -BeNullOrEmpty + $result.ComputerName | Should -Be $computerName + $result.IsSpecial | Should -Be $isSpecial + } + } + + It "Should handle special accounts properly" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-18" # Local system SID + $profilePath = "C:\WINDOWS\system32\config\systemprofile" + $isOrphaned = $false + $computerName = "Server01" + $isSpecial = $true + + # Act + $result = New-UserProfileObject -SID $sid -ProfilePath $profilePath -IsOrphaned $isOrphaned -ComputerName $computerName -IsSpecial $isSpecial + + # Assert + $result.GetType().name | Should -Be 'UserProfile' + $result.SID | Should -Be $sid + $result.ProfilePath | Should -Be $profilePath + $result.IsOrphaned | Should -Be $isOrphaned + $result.OrphanReason | Should -BeNullOrEmpty + $result.ComputerName | Should -Be $computerName + $result.IsSpecial | Should -Be $isSpecial + } + } + } +} From 4344a93d4a7ae7d81875330764d9bfd6b82cf4c2 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 11 Sep 2024 01:03:53 -0700 Subject: [PATCH 08/11] add `Remove-RegistryKeyForSID` unit tests --- source/Private/Remove-RegistryKeyForSID.ps1 | 15 ++- .../Remove-RegistryKeyForSID.tests.ps1 | 111 ++++++++++++++++++ 2 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/Private/Remove-RegistryKeyForSID.tests.ps1 diff --git a/source/Private/Remove-RegistryKeyForSID.ps1 b/source/Private/Remove-RegistryKeyForSID.ps1 index 6c0ccfc..302632b 100644 --- a/source/Private/Remove-RegistryKeyForSID.ps1 +++ b/source/Private/Remove-RegistryKeyForSID.ps1 @@ -19,7 +19,7 @@ function Remove-RegistryKeyForSID { - #Deletes a single registry key for a SID. + # Deletes a single registry key for a SID. [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $true)] @@ -34,8 +34,17 @@ function Remove-RegistryKeyForSID try { - # Use the general Remove-RegistrySubKey function to delete the SID's subkey - return Remove-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID -ComputerName $ComputerName + # Check if ShouldProcess is approved (with -WhatIf and -Confirm support) + if ($PSCmdlet.ShouldProcess("SID: $SID on $ComputerName", "Remove registry key")) + { + # Use the general Remove-RegistrySubKey function to delete the SID's subkey + return Remove-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID -ComputerName $ComputerName -Confirm:$false + } + else + { + Write-Verbose "Removal of registry key for SID '$SID' was skipped." + return $false + } } catch { diff --git a/tests/Unit/Private/Remove-RegistryKeyForSID.tests.ps1 b/tests/Unit/Private/Remove-RegistryKeyForSID.tests.ps1 new file mode 100644 index 0000000..823c548 --- /dev/null +++ b/tests/Unit/Private/Remove-RegistryKeyForSID.tests.ps1 @@ -0,0 +1,111 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe "Remove-RegistryKeyForSID" -Tag 'Private' { + + Context "When the SID registry key is successfully removed" { + + It "Should delete the registry key when confirmed" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + $computerName = "Server01" + + Mock Remove-RegistrySubKey { + return $true + } + + # Act + $result = Remove-RegistryKeyForSID -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -Confirm:$false + + # Assert + $result | Should -Be $true + Assert-MockCalled Remove-RegistrySubKey -Exactly 1 -Scope It -ParameterFilter { + $ParentKey -eq $profileListKey -and $SubKeyName -eq $sid -and $ComputerName -eq $computerName + } + } + } + } + + Context "When using -WhatIf parameter" { + + It "Should simulate deletion and not call Remove-RegistrySubKey" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + $computerName = "Server01" + + Mock Remove-RegistrySubKey + + # Act + Remove-RegistryKeyForSID -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -WhatIf + + # Assert + Assert-MockCalled Remove-RegistrySubKey -Exactly 0 -Scope It + } + } + } + + Context "When an error occurs while deleting the registry key" { + + It "Should return $false and log an error" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + $computerName = "Server01" + + Mock Remove-RegistrySubKey { throw "Registry access error" } + + Mock Write-Error + + # Act + $result = Remove-RegistryKeyForSID -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -Confirm:$false + + # Assert + $result | Should -Be $false + Assert-MockCalled Remove-RegistrySubKey -Exactly 1 -Scope It + Assert-MockCalled Write-Error -Exactly 1 -Scope It + } + } + } + + Context "When SID does not exist in the registry" { + + It "Should return $false if the registry key is not found" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + $computerName = "Server01" + + Mock Remove-RegistrySubKey { return $false } + + # Act + $result = Remove-RegistryKeyForSID -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -Confirm:$false + + # Assert + $result | Should -Be $false + Assert-MockCalled Remove-RegistrySubKey -Exactly 1 -Scope It + } + } + } +} From e765879b5500696c6a217cb83fd3464808892e06 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 11 Sep 2024 01:19:28 -0700 Subject: [PATCH 09/11] add `Remove-SIDProfile` unit tests --- source/Private/Remove-SIDProfile.ps1 | 91 ++++++++--- .../Unit/Private/Remove-SIDProfile.tests.ps1 | 146 ++++++++++++++++++ 2 files changed, 216 insertions(+), 21 deletions(-) create mode 100644 tests/Unit/Private/Remove-SIDProfile.tests.ps1 diff --git a/source/Private/Remove-SIDProfile.ps1 b/source/Private/Remove-SIDProfile.ps1 index dd35427..a8caf96 100644 --- a/source/Private/Remove-SIDProfile.ps1 +++ b/source/Private/Remove-SIDProfile.ps1 @@ -1,24 +1,44 @@ <# .SYNOPSIS - Removes a profile for a specific SID by deleting its registry key. + Coordinates the deletion of a profile registry key for a given SID. + .DESCRIPTION - The Remove-SIDProfile function attempts to delete the profile registry key for the specified SID. It returns a ProfileDeletionResult object indicating whether the deletion was successful or not. + The Remove-SIDProfile function removes the registry key associated with a specific Security Identifier (SID) from the ProfileList on the specified computer. It supports confirmation prompts and -WhatIf scenarios by using the ShouldProcess pattern. The function also handles errors that occur during the deletion process and returns a ProfileDeletionResult object indicating success or failure. + .PARAMETER SID - The Security Identifier (SID) for the profile to be removed. + The Security Identifier (SID) of the profile to be deleted. + .PARAMETER ProfileListKey - The opened registry key for the ProfileList where the profile's SID is located. + The registry key representing the ProfileList from which the SID's registry key will be removed. + .PARAMETER ComputerName - The name of the computer where the registry key will be deleted. + The name of the computer where the profile registry key resides. Defaults to the current computer. + .PARAMETER ProfilePath - The file path of the profile to be deleted. + The file path of the profile to be deleted, used for logging purposes in the ProfileDeletionResult object. + +.OUTPUTS + [ProfileDeletionResult] + An object that indicates whether the profile registry key was successfully deleted or if the action was skipped or failed. Includes the SID, ProfilePath, DeletionSuccess status, DeletionMessage, and ComputerName. + .EXAMPLE Remove-SIDProfile -SID "S-1-5-21-123456789-1001" -ProfileListKey $profileListKey -ComputerName "Server01" -ProfilePath "C:\Users\John" - Removes the profile associated with the specified SID from "Server01". + Removes the registry key for the specified SID from the ProfileList on "Server01" and deletes the profile. + +.EXAMPLE + Remove-SIDProfile -SID "S-1-5-21-123456789-1001" -ProfileListKey $profileListKey -ProfilePath "C:\Users\John" -WhatIf + Simulates the removal of the profile registry key for the specified SID using the -WhatIf parameter, showing what would have been done without performing the action. + +.NOTES + - The function supports 'ShouldProcess', allowing the use of -WhatIf and -Confirm parameters for safety. + - In case of an error, the function returns a ProfileDeletionResult object with DeletionSuccess set to $false and logs the error message. + - If the action is skipped (e.g., due to -WhatIf or confirmation denial), the function returns a ProfileDeletionResult with a status indicating that the action was skipped. #> function Remove-SIDProfile { - #Coordinates the registry key deletion and provides a result for a single SID. + [outputtype([ProfileDeletionResult])] + # Coordinates the registry key deletion and provides a result for a single SID. [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [string]$SID, @@ -27,26 +47,55 @@ function Remove-SIDProfile [string]$ProfilePath ) - # Attempt to remove the registry key - $deletionSuccess = Remove-RegistryKeyForSID -SID $SID -ProfileListKey $ProfileListKey -ComputerName $ComputerName - - if ($deletionSuccess) + try { - return [ProfileDeletionResult]::new( - $SID, - $ProfilePath, - $true, - "Profile registry key for SID '$SID' successfully deleted.", - $ComputerName - ) + # Use ShouldProcess to check if the action should proceed (with -WhatIf and -Confirm support) + if ($PSCmdlet.ShouldProcess("SID: $SID on $ComputerName", "Remove profile registry key")) + { + # Attempt to remove the registry key + $deletionSuccess = Remove-RegistryKeyForSID -SID $SID -ProfileListKey $ProfileListKey -ComputerName $ComputerName + + if ($deletionSuccess) + { + return [ProfileDeletionResult]::new( + $SID, + $ProfilePath, + $true, + "Profile registry key for SID '$SID' successfully deleted.", + $ComputerName + ) + } + else + { + return [ProfileDeletionResult]::new( + $SID, + $ProfilePath, + $false, + "Failed to delete the profile registry key for SID '$SID'.", + $ComputerName + ) + } + } + else + { + Write-Verbose "Removal of profile registry key for SID '$SID' on '$ComputerName' was skipped." + return [ProfileDeletionResult]::new( + $SID, + $ProfilePath, + $false, + "Action skipped.", + $ComputerName + ) + } } - else + catch { + Write-Error "Failed to remove the profile registry key for SID '$SID' on $ComputerName. Error: $_" return [ProfileDeletionResult]::new( $SID, $ProfilePath, $false, - "Failed to delete the profile registry key for SID '$SID'.", + "Failed to delete the profile registry key for SID '$SID'. Error: $_", $ComputerName ) } diff --git a/tests/Unit/Private/Remove-SIDProfile.tests.ps1 b/tests/Unit/Private/Remove-SIDProfile.tests.ps1 new file mode 100644 index 0000000..590f5e4 --- /dev/null +++ b/tests/Unit/Private/Remove-SIDProfile.tests.ps1 @@ -0,0 +1,146 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe "Remove-SIDProfile" -Tag 'Private' { + + Context "When the SID profile registry key is successfully removed" { + + It "Should return a successful ProfileDeletionResult when the key is deleted" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + $computerName = "Server01" + $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + + Mock Remove-RegistryKeyForSID { return $true } + + # Act + $result = Remove-SIDProfile -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -ProfilePath $profilePath -Confirm:$false + + # Assert + $result.GetType().Name | Should -Be 'ProfileDeletionResult' + $result.SID | Should -Be $sid + $result.ProfilePath | Should -Be $profilePath + $result.DeletionSuccess | Should -Be $true + $result.DeletionMessage | Should -Be "Profile registry key for SID '$sid' successfully deleted." + $result.ComputerName | Should -Be $computerName + } + } + + It "Should return a failed ProfileDeletionResult when the key is not deleted" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + $computerName = "Server01" + $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + + Mock Remove-RegistryKeyForSID { return $false } + + # Act + $result = Remove-SIDProfile -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -ProfilePath $profilePath -confirm:$false + + # Assert + $result.GetType().Name | Should -Be 'ProfileDeletionResult' + $result.SID | Should -Be $sid + $result.ProfilePath | Should -Be $profilePath + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be "Failed to delete the profile registry key for SID '$sid'." + $result.ComputerName | Should -Be $computerName + } + } + } + + Context "When using -WhatIf" { + + It "Should simulate deletion and not call Remove-RegistryKeyForSID" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + $computerName = "Server01" + $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + + Mock Remove-RegistryKeyForSID + + # Act + $result = Remove-SIDProfile -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -ProfilePath $profilePath -WhatIf + + # Assert + Assert-MockCalled Remove-RegistryKeyForSID -Exactly 0 -Scope It + $result.GetType().Name | Should -Be 'ProfileDeletionResult' + $result.SID | Should -Be $sid + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be "Action skipped." + } + } + } + + Context "When ShouldProcess returns false" { + + It "Should skip deletion and return a skipped ProfileDeletionResult" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + $computerName = "Server01" + $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + + Mock Remove-RegistryKeyForSID + + # Act + $result = Remove-SIDProfile -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -ProfilePath $profilePath -WhatIf + + # Assert + Assert-MockCalled Remove-RegistryKeyForSID -Exactly 0 -Scope It + $result.GetType().Name | Should -Be 'ProfileDeletionResult' + $result.SID | Should -Be $sid + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be "Action skipped." + } + } + } + + Context "When an error occurs during deletion" { + + It "Should return a failed ProfileDeletionResult and log an error" { + InModuleScope -ScriptBlock { + # Arrange + $sid = "S-1-5-21-123456789-1001" + $profilePath = "C:\Users\John" + $computerName = "Server01" + $profileListKey = New-MockObject -Type 'Microsoft.Win32.RegistryKey' -Properties @{ Name = "ProfileList" } + + Mock Remove-RegistryKeyForSID { throw "Registry access error" } + #Mock Write-Error + + # Act + $result = Remove-SIDProfile -SID $sid -ProfileListKey $profileListKey -ComputerName $computerName -ProfilePath $profilePath -confirm:$false -ErrorAction Continue + + # Assert + $result.GetType().Name | Should -Be 'ProfileDeletionResult' + $result.SID | Should -Be $sid + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be "Failed to delete the profile registry key for SID '$sid'. Error: Registry access error" + #Assert-MockCalled Write-Error -Exactly 1 -Scope It + } + } + } +} From 64ef924f9e4e3b7206639a5e7f2cc7b8d5a72a8f Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 11 Sep 2024 01:42:48 -0700 Subject: [PATCH 10/11] Changed `build.yaml`, `CHANGELOG.md` --- CHANGELOG.md | 2 + README.md | 168 ++++++++++++++++++++++--- RequiredModules.psd1 | 12 +- build.yaml | 4 +- source/Private/Test-SpecialAccount.ps1 | 23 ++-- 5 files changed, 170 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f35385..e94b1b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,3 +30,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Test-FolderExists` - `Test-OrphanedProfile` - `Test-SpecialAccount` + +- Implemented and completed Unit Tests for private functions diff --git a/README.md b/README.md index c2780d4..23bff61 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,156 @@ + # WinProfileOps -The WinProfileOps module provides an essential toolkit for managing Windows user profiles across local and remote computers. This module automates complex profile management tasks such as detecting orphaned profiles, validating profile paths, and removing stale or corrupted profiles. It handles both filesystem and registry operations, leveraging its dependency on WinRegOps for registry-related functions. +

+ WinProfileOps Icon +

+ +The **WinProfileOps** module provides a robust toolkit for managing Windows user +profiles on both local and remote computers. This module simplifies and automates +complex profile management tasks, such as detecting orphaned profiles, validating +profile paths, and removing stale or corrupted profiles. It handles both filesystem +and registry operations, utilizing the **WinRegOps** module for registry-related +functions. + +**WinProfileOps** seamlessly integrates with **WinRegOps** to manage profiles by +querying, validating, and deleting user profile-related data from the Windows +registry. This module is ideal for system administrators who want to streamline +profile management operations, especially in environments with numerous users and +computers. + +--- + +## Dependencies + +- **WinRegOps**: The **WinProfileOps** module depends on +[**WinRegOps**](https://github.com/LarryWisherMan/WinRegOps) for registry + operations such as querying, opening, and modifying registry keys related to user + profiles. + +--- + +## Key Features + +- **Retrieve user profile information** from both the registry and the file system + (local and remote). +- **Detect orphaned profiles**, such as profiles missing from the file system or + registry. +- **Remove orphaned or unused profiles** from the system safely. +- **Filter and exclude special accounts** like system or service accounts (e.g., + `defaultuser0`, `S-1-5-18`). +- **Remote profile management** with support for handling user profiles across + different systems. +- **Error handling** for permission issues, unreachable systems, and missing data. +- **Class-based profile objects** for easy integration with other automation tasks + or scripts. + +--- + +## Typical Use Cases + +- **Cleaning up orphaned profiles** after system migrations, user deactivations, or + profile corruption. +- **Automating stale profile removal** on both local and remote systems to save disk + space and improve performance. +- **Managing user profiles in large-scale environments**, such as terminal servers, + Citrix environments, or multi-user systems. +- **Excluding system accounts** from profile cleanup operations to prevent accidental + deletion of important system profiles. +- **System maintenance routines** that include profile validation and management as + part of a broader system health check. + +--- + +## Installation + +You have two options to install **WinProfileOps**: + +1. **Install from PowerShell Gallery** + You can install the module directly from the + [PowerShell Gallery](https://www.powershellgallery.com/packages/WinProfileOps) + using the `Install-Module` command: + + ```powershell + Install-Module -Name WinProfileOps + ``` + +1. **Install from GitHub Releases** + You can also download the latest release from the + [GitHub Releases page](https://github.com/LarryWisherMan/WinProfileOps/releases). + Download the `.zip` file, extract it, and place it in one of your `$PSModulePath` + directories. + +--- + +## Usage + +#### Example 1: Detecting Orphaned Profiles + +Use the `Get-OrphanedProfiles` function to detect orphaned profiles on a local or +remote machine: + +```powershell +$orphanedProfiles = Get-OrphanedProfiles -ComputerName "RemotePC" -IgnoreSpecial +``` + +This retrieves all orphaned profiles on `RemotePC`, excluding special accounts. + +#### Example 2: Removing Orphaned Profiles + +The `Remove-OrphanedProfiles` function allows you to remove orphaned profiles from +a system: + +```powershell +Remove-OrphanedProfiles -ComputerName "RemotePC" -WhatIf +``` + +This will show what would happen if the orphaned profiles on `RemotePC` were +deleted, without performing the deletion. + +#### Example 3: Retrieving User Profiles from the Registry + +Use the `Get-UserProfilesFromRegistry` function to query user profiles from the +Windows registry: + +```powershell +$registryProfiles = Get-UserProfilesFromRegistry -ComputerName "LocalHost" +``` + +This retrieves user profiles from the registry on `LocalHost`. + +#### Example 4: Removing a Specific Profile + +You can remove a specific profile from the registry using `Remove-SIDProfile`: + +```powershell +Remove-SIDProfile -SID "S-1-5-21-123456789-1001" -ComputerName "Server01" +``` + +This removes the registry key for the profile associated with the specified SID on +`Server01`. + +--- + +## Key Functions -WinProfileOps integrates with WinRegOps to seamlessly manage profiles by querying, validating, and deleting user profile-related data from the Windows registry. This module is ideal for system administrators seeking to streamline profile management operations, especially in environments with numerous users and computers. +- **`Get-OrphanedProfiles`**: Detects orphaned profiles by checking both the + registry and file system. +- **`Remove-OrphanedProfiles`**: Safely removes orphaned profiles, with support for + `-WhatIf` and `-Confirm`. +- **`Get-UserProfilesFromRegistry`**: Retrieves user profiles from the Windows + registry. +- **`Get-UserProfilesFromFolders`**: Retrieves user profile folders from the file + system. +- **`Remove-SIDProfile`**: Removes a user profile from the registry based on the + SID. +- **`Test-SpecialAccount`**: Checks if a user profile is considered special or + system-related. -Dependencies: -- WinRegOps: The WinProfileOps module depends on WinRegOps for registry operations such as querying, opening, and modifying registry keys related to user profiles. +--- -Key features: -- Retrieve user profile information from both the registry and file system (local and remote). -- Detect orphaned profiles (e.g., missing profile folders or registry entries). -- Remove orphaned or unused profiles from the system. -- Filter and exclude special accounts like system or service accounts. -- Built-in support for remote profile management. -- Error handling for permission issues or unreachable systems. -- Class-based profile objects for easy integration with other automation tasks. +## Contributing -Typical use cases include: -- Cleaning up orphaned user profiles after system migrations or user deactivations. -- Automating the detection and removal of stale profiles on local and remote systems. -- Managing user profiles in large-scale, multi-user environments (e.g., terminal servers, Citrix environments). -- Excluding system accounts from profile cleanup operations, ensuring important profiles remain intact. -- Providing profile management capabilities as part of system maintenance routines. +Contributions are welcome! Feel free to fork the repository, submit pull requests, +or report issues. You can contribute by adding new features, improving the existing +code, or enhancing the documentation. diff --git a/RequiredModules.psd1 b/RequiredModules.psd1 index dffe68b..ea52d9c 100644 --- a/RequiredModules.psd1 +++ b/RequiredModules.psd1 @@ -19,14 +19,6 @@ ChangelogManagement = 'latest' Sampler = 'latest' 'Sampler.GitHubTasks' = 'latest' - #'WisherTools.Helpers' = 'latest' - 'WinRegOps' = @{ - Version = '0.3.0-preview0003' - Parameters = @{ - AllowPrerelease = $true - Repository = "PSGallery" - } - } - - + 'WisherTools.Helpers' = 'latest' + 'WinRegOps' = '0.3.0' } diff --git a/build.yaml b/build.yaml index 279f478..e8f5644 100644 --- a/build.yaml +++ b/build.yaml @@ -5,7 +5,7 @@ # Path to the Module Manifest to build (where path will be resolved from) # SourcePath: ./Sampler/Sampler.psd1 # Output Directory where ModuleBuilder will build the Module, relative to module manifest -OutputDirectory: ../output/module +#OutputDirectory: ../output/module BuiltModuleSubdirectory: module CopyPaths: - en-US @@ -103,7 +103,7 @@ Pester: # - tests/Unit # - tests/Integration ExcludeTag: - - helpQuality + #- helpQuality - FunctionalQuality - TestQuality Tag: diff --git a/source/Private/Test-SpecialAccount.ps1 b/source/Private/Test-SpecialAccount.ps1 index 6c191af..98597dd 100644 --- a/source/Private/Test-SpecialAccount.ps1 +++ b/source/Private/Test-SpecialAccount.ps1 @@ -1,19 +1,24 @@ <# .SYNOPSIS - Tests if a profile is a special account. + Tests if a profile is considered a special or default account. .DESCRIPTION - The Test-SpecialAccount function checks whether the profile is a special or default account by evaluating the folder name, SID, and profile path against a predefined list of ignored accounts, SIDs, and paths. -.PARAM FolderName - The folder name of the profile being tested. -.PARAM SID + The Test-SpecialAccount function checks whether the profile is a special or default account by comparing the folder name, Security Identifier (SID), and profile path to predefined lists of ignored accounts, SIDs, and paths. + If the profile matches any of the predefined entries, it is considered a special account. +.PARAMETER FolderName + The name of the folder representing the profile being tested. +.PARAMETER SID The Security Identifier (SID) of the profile being tested. -.PARAM ProfilePath - The file path of the profile folder. +.PARAMETER ProfilePath + The file path of the profile being tested. .EXAMPLE Test-SpecialAccount -FolderName "DefaultAppPool" -SID "S-1-5-18" -ProfilePath "C:\WINDOWS\system32\config\systemprofile" - Checks if the profile associated with "DefaultAppPool" is a special account. + Checks if the profile associated with the folder "DefaultAppPool", SID "S-1-5-18", and profile path "C:\WINDOWS\system32\config\systemprofile" is a special account. +.EXAMPLE + Test-SpecialAccount -FolderName "JohnDoe" -SID "S-1-5-21-123456789-1001" -ProfilePath "C:\Users\JohnDoe" + Tests a non-special account, which does not match any predefined special accounts. +.NOTES + This function returns $true if the account is considered special, and $false otherwise. #> - function Test-SpecialAccount { param ( From ec96265fff2f3a213ee00ecc7e9c16c10053451b Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 11 Sep 2024 01:50:42 -0700 Subject: [PATCH 11/11] fix 'Test-FolderExists.tests.ps1` --- source/Private/Test-FolderExists.ps1 | 1 + tests/Unit/Private/Test-FolderExists.tests.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/source/Private/Test-FolderExists.ps1 b/source/Private/Test-FolderExists.ps1 index 1547a7e..10ea573 100644 --- a/source/Private/Test-FolderExists.ps1 +++ b/source/Private/Test-FolderExists.ps1 @@ -33,6 +33,7 @@ function Test-FolderExists { [outputType([bool])] + [cmdletbinding()] param ( [string]$ProfilePath, [string]$ComputerName = $env:COMPUTERNAME diff --git a/tests/Unit/Private/Test-FolderExists.tests.ps1 b/tests/Unit/Private/Test-FolderExists.tests.ps1 index f50708a..484ac0c 100644 --- a/tests/Unit/Private/Test-FolderExists.tests.ps1 +++ b/tests/Unit/Private/Test-FolderExists.tests.ps1 @@ -198,7 +198,7 @@ Describe "Test-FolderExists" -Tag "Private" { $computerName = $env:COMPUTERNAME # Act and Assert - $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName -ErrorAction Continue $result | Should -BeFalse Assert-MockCalled Get-DirectoryPath -Exactly 1 @@ -214,7 +214,7 @@ Describe "Test-FolderExists" -Tag "Private" { $computerName = $env:COMPUTERNAME # Act and Assert - $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName + $result = Test-FolderExists -ProfilePath $profilePath -ComputerName $computerName -ErrorAction Continue $result | Should -BeFalse Assert-MockCalled Get-DirectoryPath -Exactly 1