From 0a7dacbc676dca344a33a63a93fc536bde082b32 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 16 Sep 2024 15:29:10 -0700 Subject: [PATCH 01/23] fixed foldername bug in `Process-RegistryProfiles` --- CHANGELOG.md | 5 +++++ source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aca1384..be6ec12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- removed bug from `Process-RegistryProfiles` regarding populating the +FolderName variable + ### Added - New helper function `Validate-SIDFormat` to verify SID value upon retrieval diff --git a/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 b/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 index cde53b1..46dfe89 100644 --- a/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 +++ b/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 @@ -53,6 +53,7 @@ function Process-RegistryProfiles $folderExists = $null $accessError = $false + $folderName = Split-Path -Path $profilePath -Leaf $isSpecial = Test-SpecialAccount -FolderName $folderName -SID $regProfile.SID -ProfilePath $profilePath if ($IgnoreSpecial -and $isSpecial) @@ -75,7 +76,7 @@ function Process-RegistryProfiles Write-Warning "Error testing folder existence for profile: $profilePath. Error: $_" } - $folderName = Split-Path -Path $profilePath -Leaf + $userProfile = Test-OrphanedProfile -SID $regProfile.SID -ProfilePath $profilePath ` -FolderExists $folderExists -AccessError $accessError -IgnoreSpecial $IgnoreSpecial ` From 541596cde5b53681bce3dae0f83eeb94c6caf1d7 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Tue, 17 Sep 2024 09:22:35 -0700 Subject: [PATCH 02/23] updated required modules --- RequiredModules.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RequiredModules.psd1 b/RequiredModules.psd1 index b647772..39084f0 100644 --- a/RequiredModules.psd1 +++ b/RequiredModules.psd1 @@ -23,7 +23,7 @@ #'WinRegOps' = '0.3.0' 'WinRegOps' = @{ - Version = '0.4.0-preview0001' + Version = '0.4.0-preview0002' Parameters = @{ AllowPrerelease = $true Repository = "PSGallery" From 4e2f10c48c26fd818cfa70dc56f635ceba070230 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Tue, 17 Sep 2024 12:54:03 -0700 Subject: [PATCH 03/23] add integration tests --- .../NotImplemented/Remove-ProfilesForSIDs.ps1 | 67 +++ source/Private/Get-SIDProfileInfo.ps1 | 9 +- source/Private/Remove-RegistryKeyForSID.ps1 | 2 +- source/Private/Test-OrphanedProfile.ps1 | 5 + .../Process-RegistryProfiles.ps1 | 14 +- source/prefix.ps1 | 6 +- tests/Intergration/PublicFunctions.tests.ps1 | 459 ++++++++++++++++++ 7 files changed, 556 insertions(+), 6 deletions(-) create mode 100644 tests/Intergration/PublicFunctions.tests.ps1 diff --git a/source/NotImplemented/Remove-ProfilesForSIDs.ps1 b/source/NotImplemented/Remove-ProfilesForSIDs.ps1 index a812fde..2f2dd69 100644 --- a/source/NotImplemented/Remove-ProfilesForSIDs.ps1 +++ b/source/NotImplemented/Remove-ProfilesForSIDs.ps1 @@ -90,3 +90,70 @@ function Remove-ProfilesForSIDs # Return the array of deletion results return $deletionResults } + + +function Remove-ProfilesForSIDs { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory = $true)] + [string[]]$SIDs, # Accept multiple SIDs as an array + + [Parameter(Mandatory = $false)] + [string]$ComputerName = $env:COMPUTERNAME # Default to local computer + ) + + # Base registry path for profiles + $RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + $deletionResults = @() + + # Loop through each SID and process deletion + foreach ($sid in $SIDs) { + try { + # Full path to the profile SID key + $fullRegistryPath = Join-Path -Path $RegistryPath -ChildPath $sid + + # Check if the profile key exists before trying to delete + $profileKeyExists = Test-Path "HKLM:\$fullRegistryPath" + + if (-not $profileKeyExists) { + # If the profile does not exist, add a result indicating it wasn't found + $deletionResults += [PSCustomObject]@{ + SID = $sid + ProfilePath = $null + Success = $false + Message = "Profile SID '$sid' not found in registry." + Computer = $ComputerName + } + continue + } + + # Attempt to remove the profile using Remove-RegistrySubKey + Remove-RegistrySubKey -RegistryHive 'LocalMachine' -RegistryPath $RegistryPath -SubKeyName $SID -ComputerName $ComputerName -ThrowOnMissingSubKey $false + + # Add a result indicating success + $deletionResults += [PSCustomObject]@{ + SID = $sid + ProfilePath = $fullRegistryPath + Success = $true + Message = "Profile SID '$sid' removed successfully." + Computer = $ComputerName + } + } + catch { + # Handle any errors that occur during deletion + Write-Error "An error occurred while processing SID '$sid'. $_" + + # Add a result indicating failure due to an error + $deletionResults += [PSCustomObject]@{ + SID = $sid + ProfilePath = $null + Success = $false + Message = "Error occurred while processing SID '$sid'. Error: $_" + Computer = $ComputerName + } + } + } + + # Return the array of deletion results + return $deletionResults +} diff --git a/source/Private/Get-SIDProfileInfo.ps1 b/source/Private/Get-SIDProfileInfo.ps1 index bd3a9f6..a20a56e 100644 --- a/source/Private/Get-SIDProfileInfo.ps1 +++ b/source/Private/Get-SIDProfileInfo.ps1 @@ -11,6 +11,9 @@ .PARAMETER ComputerName The name of the computer from which to retrieve profile information. Defaults to the local computer. +.parameter RegistryPath + The registry path to the ProfileList key. Defaults to "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList". + .EXAMPLE Get-SIDProfileInfo -ComputerName "Server01" Retrieves profile information for all valid SIDs stored in the registry on "Server01". @@ -39,11 +42,11 @@ function Get-SIDProfileInfo [OutputType([PSCustomObject[]])] [CmdletBinding()] param ( - [string]$ComputerName = $env:COMPUTERNAME + [string]$ComputerName = $env:COMPUTERNAME, + [string]$RegistryPath = $env:GetSIDProfileInfo_RegistryPath ) - $RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" - $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName -Writable $false + $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName -Writable $false -RegistryHive $env:GetSIDProfile_RegistryHive # Handle null or empty registry key if (-not $ProfileListKey) diff --git a/source/Private/Remove-RegistryKeyForSID.ps1 b/source/Private/Remove-RegistryKeyForSID.ps1 index 302632b..42e5ee2 100644 --- a/source/Private/Remove-RegistryKeyForSID.ps1 +++ b/source/Private/Remove-RegistryKeyForSID.ps1 @@ -38,7 +38,7 @@ function Remove-RegistryKeyForSID 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 + return Remove-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID -Confirm:$false } else { diff --git a/source/Private/Test-OrphanedProfile.ps1 b/source/Private/Test-OrphanedProfile.ps1 index bcf1b55..4db3fd1 100644 --- a/source/Private/Test-OrphanedProfile.ps1 +++ b/source/Private/Test-OrphanedProfile.ps1 @@ -48,6 +48,11 @@ function Test-OrphanedProfile return New-UserProfileObject -SID $SID -ProfilePath $ProfilePath -IsOrphaned $true ` -OrphanReason "MissingFolder" -ComputerName $ComputerName -IsSpecial $IsSpecial } + elseif (-not $ProfilePath -and -not $FolderExists) + { + return New-UserProfileObject -SID $SID -ProfilePath $null -IsOrphaned $true ` + -OrphanReason "MissingProfileImagePathAndFolder" -ComputerName $ComputerName -IsSpecial $IsSpecial + } else { return New-UserProfileObject -SID $SID -ProfilePath $ProfilePath -IsOrphaned $false ` diff --git a/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 b/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 index 46dfe89..f5d1bb5 100644 --- a/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 +++ b/source/Private/UserProfileAudit/Process-RegistryProfiles.ps1 @@ -50,9 +50,21 @@ function Process-RegistryProfiles foreach ($regProfile in $RegistryProfiles) { $profilePath = $regProfile.ProfilePath - $folderExists = $null + $folderExists = $false $accessError = $false + if ($null -eq $profilePath) + { + $isSpecial = Test-SpecialAccount -SID $regProfile.SID + $userProfile = Test-OrphanedProfile -SID $regProfile.SID -ProfilePath $null ` + -FolderExists $folderExists -AccessError $accessError -IgnoreSpecial $IgnoreSpecial ` + -IsSpecial $isSpecial -ComputerName $ComputerName + + # Add this line to include the user profile in the processed array + $processedProfiles += $userProfile + continue + } + $folderName = Split-Path -Path $profilePath -Leaf $isSpecial = Test-SpecialAccount -FolderName $folderName -SID $regProfile.SID -ProfilePath $profilePath diff --git a/source/prefix.ps1 b/source/prefix.ps1 index 38443f8..ade5304 100644 --- a/source/prefix.ps1 +++ b/source/prefix.ps1 @@ -6,6 +6,9 @@ $windowsIdentity = [Security.Principal.WindowsIdentity]::GetCurrent() $windowsPrincipal = New-Object Security.Principal.WindowsPrincipal($windowsIdentity) $isAdmin = $windowsPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +$env:GetSIDProfileInfo_RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" +$env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::LocalMachine + # Set the environment variable based on whether the user is an admin if ($isAdmin) { @@ -21,7 +24,8 @@ Write-Verbose "User is an administrator: $ENV:WinProfileOps_IsAdmin" [scriptblock]$SB = { if (Test-Path Env:\WinProfileOps_IsAdmin) { - Remove-Item Env:\WinProfileOps_IsAdmin + Remove-Item Env:\WinProfileOps_IsAdmin -errorAction SilentlyContinue + Remove-Item Env:\GetSIDProfileInfo_RegistryPath -ErrorAction SilentlyContinue Write-Verbose "WinProfileOps: Removed WinProfileOps_IsAdmin environment variable." } } diff --git a/tests/Intergration/PublicFunctions.tests.ps1 b/tests/Intergration/PublicFunctions.tests.ps1 new file mode 100644 index 0000000..575a010 --- /dev/null +++ b/tests/Intergration/PublicFunctions.tests.ps1 @@ -0,0 +1,459 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + # Import the module being tested + 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 + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force + + # Clean up environment variables + Remove-Item -Path Env:Registry_Path -ErrorAction SilentlyContinue + Remove-Item -Path Env:Export_Path -ErrorAction SilentlyContinue + +} + +Describe "PublicFuntions Tests" -Tag "Intergration" { + + Context "Get-UserProfilesFromFolders" { + + BeforeEach { + $MockOutProfilePath = mkdir "$TestDrive\Users" + $MockeFolderNames = @("User1", "User2", "User3") + + $MockedItems = $MockeFolderNames | ForEach-Object { + $FolderName = $_ + mkdir "$TestDrive\Users\$folderName" + } + } + + AfterEach { + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force + } + } + + It "Should return an array of user profile folders" { + + #$MockRegPath = "TestRegistry:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + #$MockOutRegPath = New-Item -Path $MockRegPath + + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $result = Get-UserProfilesFromFolders -ComputerName $ComputerName -ProfileFolderPath $profilePath + + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $result[0].ProfilePath | Should -Be "$ProfilePath\User1" + } + } + + Context "Get-UserProfilesFromRegistry" { + + BeforeEach { + # Ensure clean-up of TestDrive before creating folders + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force -ErrorAction SilentlyContinue + } + + # Create mock profile folders in TestDrive + $MockProfilePath = mkdir "$TestDrive\Users" + $MockUsers = @( + @{ + Foldername = "User1" + SID = "S-1-5-21-1234567890-1" + }, + @{ + Foldername = "User2" + SID = "S-1-5-21-1234567890-2" + }, + @{ + Foldername = "User3" + SID = "S-1-5-21-1234567890-3" + } + ) + + $MockUsers | ForEach-Object { + mkdir "$TestDrive\Users\$($_.Foldername)" + } + + # Mock registry entries in TestRegistry + $MockRegistryPath = "HKCU:\Software\Pester\ProfileList" + + # Create registry path if it doesn't exist + if (-not (Test-Path $MockRegistryPath)) + { + New-Item -Path $MockRegistryPath -ItemType Directory + } + + # Set up the environment variable for the registry path + $env:GetSIDProfileInfo_RegistryPath = "Software\Pester\ProfileList" + $env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser + + # Create registry items for each mock user + $MockUsers | ForEach-Object { + $SID = $_.SID + $FolderName = $_.Foldername + $RegistryItemPath = "$MockRegistryPath\$SID" + + # Create registry key and set profile path + if (-not (Test-Path $RegistryItemPath)) + { + New-Item -Path $RegistryItemPath + } + + Set-ItemProperty -Path $RegistryItemPath -Name ProfileImagePath -Value "$TestDrive\Users\$FolderName" + } + } + + + AfterEach { + # Clean up mock folders and registry items + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force + } + + if (Test-Path "HKCU:\Software\Pester\ProfileList") + { + Remove-Item -Path "HKCU:\Software\Pester\ProfileList" -Recurse + } + + #resetEnvVariables + Remove-Item -Path Env:GetSIDProfileInfo_RegistryPath -ErrorAction SilentlyContinue + Remove-Item -Path Env:GetSIDProfile_RegistryHive -ErrorAction SilentlyContinue + } + + It "Should return an array of user profiles from the registry" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-UserProfilesFromRegistry -ComputerName $ComputerName + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $result[0].ProfilePath | Should -Be "$ProfilePath\User1" + } + + + It "It should return object even if missing ProfileImagePath in Registry" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Set-ItemProperty -Path "HKCU:\Software\Pester\ProfileList\S-1-5-21-1234567890-1" -Name ProfileImagePath -Value "" + + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-UserProfilesFromRegistry $ComputerName + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $result[0].profilePath | Should -BeNullOrEmpty + $result[0].ExistsInRegistry | Should -Be $true + } + + } + + Context "Ivoke-UserProfileAudit" { + + BeforeEach { + # Ensure clean-up of TestDrive before creating folders + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force -ErrorAction SilentlyContinue + } + + # Create mock profile folders in TestDrive + $MockProfilePath = mkdir "$TestDrive\Users" + $MockUsers = @( + @{ + Foldername = "User1" + SID = "S-1-5-21-1234567890-1" + }, + @{ + Foldername = "User2" + SID = "S-1-5-21-1234567890-2" + }, + @{ + Foldername = "User3" + SID = "S-1-5-21-1234567890-3" + } + ) + + $MockUsers | ForEach-Object { + mkdir "$TestDrive\Users\$($_.Foldername)" + } + + # Mock registry entries in TestRegistry + $MockRegistryPath = "HKCU:\Software\Pester\ProfileList" + + # Create registry path if it doesn't exist + if (-not (Test-Path $MockRegistryPath)) + { + New-Item -Path $MockRegistryPath -ItemType Directory + } + + # Set up the environment variable for the registry path + $env:GetSIDProfileInfo_RegistryPath = "Software\Pester\ProfileList" + $env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser + + # Create registry items for each mock user + $MockUsers | ForEach-Object { + $SID = $_.SID + $FolderName = $_.Foldername + $RegistryItemPath = "$MockRegistryPath\$SID" + + # Create registry key and set profile path + if (-not (Test-Path $RegistryItemPath)) + { + New-Item -Path $RegistryItemPath + } + + Set-ItemProperty -Path $RegistryItemPath -Name ProfileImagePath -Value "$TestDrive\Users\$FolderName" + } + } + + + AfterEach { + # Clean up mock folders and registry items + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force + } + + if (Test-Path "HKCU:\Software\Pester\ProfileList") + { + Remove-Item -Path "HKCU:\Software\Pester\ProfileList" -Recurse + } + + #resetEnvVariables + Remove-Item -Path Env:GetSIDProfileInfo_RegistryPath -ErrorAction SilentlyContinue + Remove-Item -Path Env:GetSIDProfile_RegistryHive -ErrorAction SilentlyContinue + } + + It "It should return non orphaned Audit Objects" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $result[0].ProfilePath | Should -Be "$ProfilePath\User1" + } + + It "It should return 1 orphaned due to missing folder" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Remove-Item "$profilePath\User1" -Recurse -Force + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $result[0].OrphanReason | Should -Be "MissingFolder" + $result[0].IsOrphaned | Should -Be $true + } + + + It "It should return 1 orphaned due to missing registry entry" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Remove-Item "HKCU:\Software\Pester\ProfileList\S-1-5-21-1234567890-1" -Recurse -Force + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $profilePath + + $selected = $result | Where-Object { $_.ProfilePath -match "User1" } + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 3 + $selected.OrphanReason | Should -Be "MissingRegistryEntry" + $selected.IsOrphaned | Should -Be $true + } + + It "It should return 2 orphaned due to missing ProfileImagePath in Registry" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Set-ItemProperty -Path "HKCU:\Software\Pester\ProfileList\S-1-5-21-1234567890-1" -Name ProfileImagePath -Value "" + + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $profilePath + + + $Orphaned = $result | Where-Object { $_.IsOrphaned -eq $true } + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 4 + $Orphaned.Count | Should -Be 2 + $Orphaned[0].OrphanReason | Should -Be "MissingProfileImagePath" + $Orphaned[1].OrphanReason | Should -Be "MissingRegistryEntry" + + } + } + + + Context "Get-OrphanedProfiles" { + + BeforeEach { + # Ensure clean-up of TestDrive before creating folders + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force -ErrorAction SilentlyContinue + } + + # Create mock profile folders in TestDrive + $MockProfilePath = mkdir "$TestDrive\Users" + $MockUsers = @( + @{ + Foldername = "User1" + SID = "S-1-5-21-1234567890-1" + }, + @{ + Foldername = "User2" + SID = "S-1-5-21-1234567890-2" + }, + @{ + Foldername = "User3" + SID = "S-1-5-21-1234567890-3" + } + ) + + $MockUsers | ForEach-Object { + mkdir "$TestDrive\Users\$($_.Foldername)" + } + + # Mock registry entries in TestRegistry + $MockRegistryPath = "HKCU:\Software\Pester\ProfileList" + + # Create registry path if it doesn't exist + if (-not (Test-Path $MockRegistryPath)) + { + New-Item -Path $MockRegistryPath -ItemType Directory + } + + # Set up the environment variable for the registry path + $env:GetSIDProfileInfo_RegistryPath = "Software\Pester\ProfileList" + $env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser + + # Create registry items for each mock user + $MockUsers | ForEach-Object { + $SID = $_.SID + $FolderName = $_.Foldername + $RegistryItemPath = "$MockRegistryPath\$SID" + + # Create registry key and set profile path + if (-not (Test-Path $RegistryItemPath)) + { + New-Item -Path $RegistryItemPath + } + + Set-ItemProperty -Path $RegistryItemPath -Name ProfileImagePath -Value "$TestDrive\Users\$FolderName" + } + } + + AfterEach { + # Clean up mock folders and registry items + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force + } + + if (Test-Path "HKCU:\Software\Pester\ProfileList") + { + Remove-Item -Path "HKCU:\Software\Pester\ProfileList" -Recurse + } + + #resetEnvVariables + Remove-Item -Path Env:GetSIDProfileInfo_RegistryPath -ErrorAction SilentlyContinue + Remove-Item -Path Env:GetSIDProfile_RegistryHive -ErrorAction SilentlyContinue + } + + It "Should return null if no orphaned profiles are found" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-OrphanedProfiles -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -BeNullOrEmpty + $result.Count | Should -Be 0 + } + + It "Should return 1 orphaned profile due to missing folder" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Remove-Item "$profilePath\User1" -Recurse -Force + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-OrphanedProfiles -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 1 + $result.OrphanReason | Should -Be "MissingFolder" + $result.IsOrphaned | Should -Be $true + } + + It "Should return 1 orphaned profile due to missing registry entry" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Remove-Item "HKCU:\Software\Pester\ProfileList\S-1-5-21-1234567890-1" -Recurse -Force + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-OrphanedProfiles -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 1 + $result.OrphanReason | Should -Be "MissingRegistryEntry" + $result.IsOrphaned | Should -Be $true + } + + It "Should return 2 orphaned profile due to missing registry entry and folder" { + $ComputerName = $Env:COMPUTERNAME + $profilePath = "$TestDrive\Users" + + $null = Remove-Item "HKCU:\Software\Pester\ProfileList\S-1-5-21-1234567890-1" -Recurse -Force + $null = Remove-Item "$profilePath\User2" -Recurse -Force + # Call the function to test (this is the function that fetches profiles from the registry) + $result = Get-OrphanedProfiles -ComputerName $ComputerName -ProfileFolderPath $profilePath + + # Validate the result + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 2 + + $result[0].OrphanReason | Should -Be "MissingFolder" + $result[0].IsOrphaned | Should -Be $true + + $result[1].OrphanReason | Should -Be "MissingRegistryEntry" + $result[1].IsOrphaned | Should -Be $true + + } + + } +} From f18a02ebea6167cec8f6035512ea3773b3a9d6a2 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 18 Sep 2024 01:13:47 -0700 Subject: [PATCH 04/23] add RemoveProfileFromReg supporting functions --- RequiredModules.psd1 | 2 +- source/Classes/ProfileDeletionResult.ps1 | 8 +- source/Private/Helpers/Update-JsonFile.ps1 | 33 ++++++++ .../Invoke-ProcessProfileRemoval.ps1 | 34 ++++++++ .../New-ProfileDeletionResult.ps1 | 21 +++++ source/Public/Invoke-UserProfileAudit.ps1 | 4 + .../Remove-UserProfilesFromRegistry.ps1 | 80 +++++++++++++++++++ source/prefix.ps1 | 21 ++--- tests/Helpers/MockedProfListReg.ps1 | 67 ++++++++++++++++ 9 files changed, 251 insertions(+), 19 deletions(-) create mode 100644 source/Private/Helpers/Update-JsonFile.ps1 create mode 100644 source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 create mode 100644 source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 create mode 100644 source/Public/Remove-UserProfilesFromRegistry.ps1 create mode 100644 tests/Helpers/MockedProfListReg.ps1 diff --git a/RequiredModules.psd1 b/RequiredModules.psd1 index 39084f0..c3793c0 100644 --- a/RequiredModules.psd1 +++ b/RequiredModules.psd1 @@ -23,7 +23,7 @@ #'WinRegOps' = '0.3.0' 'WinRegOps' = @{ - Version = '0.4.0-preview0002' + Version = '0.4.0-preview0003' Parameters = @{ AllowPrerelease = $true Repository = "PSGallery" diff --git a/source/Classes/ProfileDeletionResult.ps1 b/source/Classes/ProfileDeletionResult.ps1 index 4f0a198..3b6d952 100644 --- a/source/Classes/ProfileDeletionResult.ps1 +++ b/source/Classes/ProfileDeletionResult.ps1 @@ -1,12 +1,14 @@ -class ProfileDeletionResult { +class ProfileDeletionResult +{ [string]$SID - [string]$ProfilePath + [string]$ProfilePath = $null [bool]$DeletionSuccess [string]$DeletionMessage [string]$ComputerName # Constructor to initialize the properties - ProfileDeletionResult([string]$sid, [string]$profilePath, [bool]$deletionSuccess, [string]$deletionMessage, [string]$computerName) { + ProfileDeletionResult([string]$sid, [string]$profilePath, [bool]$deletionSuccess, [string]$deletionMessage, [string]$computerName) + { $this.SID = $sid $this.ProfilePath = $profilePath $this.DeletionSuccess = $deletionSuccess diff --git a/source/Private/Helpers/Update-JsonFile.ps1 b/source/Private/Helpers/Update-JsonFile.ps1 new file mode 100644 index 0000000..3954776 --- /dev/null +++ b/source/Private/Helpers/Update-JsonFile.ps1 @@ -0,0 +1,33 @@ +function Update-JsonFile +{ + param ( + [Parameter(Mandatory = $true)] + [string]$OutputFile, + + [Parameter(Mandatory = $true)] + [array]$RegistryData # Generic data for registry keys + ) + + if (Test-Path $OutputFile) + { + # Get the existing data and convert it from JSON + $existingData = Get-Content -Path $OutputFile -Raw | ConvertFrom-Json + + # Check if existing data is an array; if not, convert it into an array + if (-not ($existingData -is [System.Collections.IEnumerable])) + { + $existingData = @(, $existingData) + } + + # Concatenate the existing data and the new data + $existingData += $RegistryData + + # Write the updated data back to the file + $existingData | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputFile + } + else + { + # Create a new JSON file with the provided registry data + $RegistryData | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputFile + } +} diff --git a/source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 b/source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 new file mode 100644 index 0000000..07cebb9 --- /dev/null +++ b/source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 @@ -0,0 +1,34 @@ +function Invoke-ProcessProfileRemoval +{ + param ( + [string]$SID, + [Microsoft.Win32.RegistryKey]$BaseKey, + [string]$RegBackUpDirectory, + [string]$ComputerName, + [object]$SelectedProfile + ) + + try + { + # Backup the registry key associated with the SID + $RegBackUpObject = New-RegistryKeyValuesObject -RegistryKey $BaseKey -ComputerName $ComputerName -SubKeyName $SID + Remove-RegistrySubKey -ParentKey $BaseKey -SubKeyName $SID -ThrowOnMissingSubKey $false + + $VerifyDeletion = ($BaseKey.GetSubKeyNames() -notcontains $SID) + + if ($VerifyDeletion) + { + Update-JsonFile -OutputFile "$RegBackUpDirectory\RegBackUp.json" -RegistryData $RegBackUpObject + return New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $true -DeletionMessage "Profile removed successfully." -ComputerName $ComputerName + } + else + { + return New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $false -DeletionMessage "Profile not removed." -ComputerName $ComputerName + } + } + catch + { + Write-Error "Error removing profile for SID $SID`: $_" + return New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $false -DeletionMessage "Error during removal." -ComputerName $ComputerName + } +} diff --git a/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 b/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 new file mode 100644 index 0000000..1d3fc23 --- /dev/null +++ b/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 @@ -0,0 +1,21 @@ +function New-ProfileDeletionResult +{ + param ( + [Parameter(Mandatory = $true)] + [string]$SID, + + [Parameter(Mandatory = $false)] + [string]$ProfilePath = $null, + + [Parameter(Mandatory = $true)] + [bool]$DeletionSuccess, + + [Parameter(Mandatory = $true)] + [string]$DeletionMessage, + + [Parameter(Mandatory = $true)] + [string]$ComputerName + ) + + return [ProfileDeletionResult]::new($SID, $ProfilePath, $DeletionSuccess, $DeletionMessage, $ComputerName) +} diff --git a/source/Public/Invoke-UserProfileAudit.ps1 b/source/Public/Invoke-UserProfileAudit.ps1 index 05f85a7..fc221b1 100644 --- a/source/Public/Invoke-UserProfileAudit.ps1 +++ b/source/Public/Invoke-UserProfileAudit.ps1 @@ -60,6 +60,10 @@ function Invoke-UserProfileAudit begin { $AllProfiles = @() + if ($null -eq $ComputerName) + { + $ComputerName = $env:COMPUTERNAME + } } process diff --git a/source/Public/Remove-UserProfilesFromRegistry.ps1 b/source/Public/Remove-UserProfilesFromRegistry.ps1 new file mode 100644 index 0000000..1c39fa9 --- /dev/null +++ b/source/Public/Remove-UserProfilesFromRegistry.ps1 @@ -0,0 +1,80 @@ +function Remove-UserProfilesFromRegistry +{ + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory = $true)] + [string[]]$SIDs, # Array of SIDs to be removed + + [Parameter(Mandatory = $false)] + [string]$ComputerName = $env:COMPUTERNAME, # Target computer + + [switch]$AuditOnly # If set, function will only audit and not remove profiles + ) + + Begin + { + + $RegistryPath = $env:GetSIDProfileInfo_RegistryPath + $ProfileFolderPath = $env:GetSIDProfileInfo_ProfileFolderPath + $RegistryHive = $env:GetSIDProfile_RegistryHive + + # Validate Registry Path Variables + if (-not $env:GetSIDProfileInfo_RegistryPath -or -not $env:GetSIDProfileInfo_ProfileFolderPath) + { + throw "Missing registry or profile folder path environment variables." + } + + try + { + # Set up for registry backup - Get the directory path, and create if it doesn't exist + $RegBackUpDirectory = Get-DirectoryPath -basePath $env:WinProfileOps_RegBackUpDirectory -ComputerName $ComputerName -IsLocal ($ComputerName -eq $env:COMPUTERNAME) + $null = Test-DirectoryExistence -Directory $RegBackUpDirectory + + # Open the registry key and audit user profiles + $BaseKey = Open-RegistryKey -RegistryHive $RegistryHive -RegistryPath $RegistryPath -ComputerName $ComputerName + $userProfileAudit = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial + + if (-not $BaseKey) + { + throw "Failed to open registry key at path: $RegistryPath" + } + } + catch + { + throw "Error in Begin block: $_" + } + + $deletionResults = @() # Initialize results array + } + + Process + { + foreach ($SID in $SIDs) + { + + $SelectedProfile = $userProfileAudit | Where-Object { $_.SID -eq $SID } + + if ($null -eq $SelectedProfile) + { + $deletionResults += New-ProfileDeletionResult -SID $SID -ProfilePath $null -DeletionSuccess $false -DeletionMessage "Profile not found." -ComputerName $ComputerName + continue + } + + if ($AuditOnly) + { + $deletionResults += New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $true -DeletionMessage "Audit only, no deletion performed." -ComputerName $ComputerName + continue + } + + if ($PSCmdlet.ShouldProcess($SID, "Remove Profile")) + { + $deletionResults += Invoke-ProcessProfileRemoval -SID $SID -BaseKey $BaseKey -RegBackUpDirectory $RegBackUpDirectory -ComputerName $ComputerName -selectedProfile $SelectedProfile + } + } + } + + End + { + return $deletionResults + } +} diff --git a/source/prefix.ps1 b/source/prefix.ps1 index ade5304..cc5caf7 100644 --- a/source/prefix.ps1 +++ b/source/prefix.ps1 @@ -1,32 +1,23 @@ # Your functions - # Check if the current user is an administrator $windowsIdentity = [Security.Principal.WindowsIdentity]::GetCurrent() $windowsPrincipal = New-Object Security.Principal.WindowsPrincipal($windowsIdentity) -$isAdmin = $windowsPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) - +$env:WinProfileOps_IsAdmin = $windowsPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) $env:GetSIDProfileInfo_RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::LocalMachine +$env:WinProfileOps_RegBackUpDirectory = "C:\LHStuff\RegBackUp" +$env:GetSIDProfileInfo_ProfileFolderPath = $env:SystemDrive + "\Users" -# Set the environment variable based on whether the user is an admin -if ($isAdmin) -{ - $ENV:WinProfileOps_IsAdmin = $true -} -else -{ - $ENV:WinProfileOps_IsAdmin = $false -} - -Write-Verbose "User is an administrator: $ENV:WinProfileOps_IsAdmin" [scriptblock]$SB = { if (Test-Path Env:\WinProfileOps_IsAdmin) { Remove-Item Env:\WinProfileOps_IsAdmin -errorAction SilentlyContinue Remove-Item Env:\GetSIDProfileInfo_RegistryPath -ErrorAction SilentlyContinue - Write-Verbose "WinProfileOps: Removed WinProfileOps_IsAdmin environment variable." + Remove-Item Env:\GetSIDProfile_RegistryHive -ErrorAction SilentlyContinue + Remove-Item Env:\WinProfileOps_RegBackUpDirectory -ErrorAction SilentlyContinue + Remove-Item Env:\GetSIDProfileInfo_ProfileFolderPath -ErrorAction SilentlyContinue } } diff --git a/tests/Helpers/MockedProfListReg.ps1 b/tests/Helpers/MockedProfListReg.ps1 new file mode 100644 index 0000000..366a103 --- /dev/null +++ b/tests/Helpers/MockedProfListReg.ps1 @@ -0,0 +1,67 @@ +Remove-Module WinProfileOps -ErrorAction SilentlyContinue +Import-Module "D:\1_Code\GithubRepos\WinProfileOps\output\module\WinProfileOps\0.0.1\WinProfileOps.psd1" -Force + + +$null = Remove-Item -Path $env:WinProfileOps_RegBackUpDirectory -Recurse -Force -ErrorAction SilentlyContinue + +$TestDrive = "C:\Temp\TestDirectory" + +if (Test-Path "$TestDrive\Users") +{ + $null = Remove-Item -Path "$TestDrive\Users" -Recurse -Force -ErrorAction SilentlyContinue +} + +# Create mock profile folders in TestDrive +$MockProfilePath = mkdir "$TestDrive\Users" +$MockUsers = @( + @{ + Foldername = "User1" + SID = "S-1-5-21-1234567890-1" + }, + @{ + Foldername = "User2" + SID = "S-1-5-21-1234567890-2" + }, + @{ + Foldername = "User3" + SID = "S-1-5-21-1234567890-3" + } +) + +$MockUsers | ForEach-Object { + $null = mkdir "$TestDrive\Users\$($_.Foldername)" +} + +# Mock registry entries in TestRegistry +$MockRegistryPath = "HKCU:\Software\Pester\ProfileList" + +# Create registry path if it doesn't exist +if (-not (Test-Path $MockRegistryPath)) +{ + $null = New-Item -Path $MockRegistryPath -ItemType Directory +} + +# Set up the environment variable for the registry path +$env:GetSIDProfileInfo_RegistryPath = "Software\Pester\ProfileList" +$env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser +$env:GetSIDProfileInfo_ProfileFolderPath = "$TestDrive\Users" + +# Create registry items for each mock user +$MockUsers | ForEach-Object { + $SID = $_.SID + $FolderName = $_.Foldername + $RegistryItemPath = "$MockRegistryPath\$SID" + + # Create registry key and set profile path + if (-not (Test-Path $RegistryItemPath)) + { + $null = New-Item -Path $RegistryItemPath + } + + $null = Set-ItemProperty -Path $RegistryItemPath -Name ProfileImagePath -Value "$TestDrive\Users\$FolderName" +} + +$ProfileFolderPath = $env:GetSIDProfileInfo_ProfileFolderPath +$userProfileAudit = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial + +$out = Remove-UserProfilesFromRegistry -SIDs "S-1-5-21-1234567890-1", "S-1-5-21-1234567890-2" -Confirm:$false From f5fab91564c3db64394ae3563ab20cd7da3b9648 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Wed, 18 Sep 2024 23:25:04 -0700 Subject: [PATCH 05/23] Saving Work - I'm so bad --- .vscode/analyzersettings.psd1 | 4 +- source/Classes/ProfileDeletionResult.ps1 | 37 ++- source/Classes/UserProfile.ps1 | 4 + .../Get-UserProfileLastUseTime.ps1 | 41 +++ .../Private/Helpers/New-DirectoryIfNeeded.ps1 | 23 ++ .../Helpers/Resolve-UsernamesToSIDs.ps1 | 22 ++ .../Helpers/Test-EnvironmentVariable.ps1 | 36 +++ source/Private/Helpers/Update-JsonFile.ps1 | 2 +- .../Backup-RegistryKeyForSID.ps1 | 44 +++ .../Confirm-ProfileRemoval.ps1 | 17 ++ .../Invoke-ProcessProfileRemoval.ps1 | 62 +++- .../Invoke-SingleProfileAction.ps1 | 81 ++++++ .../Invoke-UserProfileProcessing.ps1 | 87 ++++++ .../New-ProfileDeletionResult.ps1 | 36 ++- .../Remove-ProfileRegistryKey.ps1 | 18 ++ .../Resolve-UserProfileForDeletion.ps1 | 36 +++ .../Test-FolderExists.ps1 | 0 .../Test-OrphanedProfile.ps1 | 0 .../Test-SpecialAccount.ps1 | 0 .../Validate-SIDFormat.ps1 | 0 .../Remove-UserProfilesFromRegistry.ps1 | 79 +++-- .../Test-FolderExists.tests.ps1 | 0 .../Test-OrphanedProfile.tests.ps1 | 0 .../Test-SpecialAccount.tests.ps1 | 0 .../Validate-SIDFormat.tests.ps1 | 0 .../Remove-UserProfilesFromRegistry.tests.ps1 | 269 ++++++++++++++++++ 26 files changed, 825 insertions(+), 73 deletions(-) create mode 100644 source/NotImplemented/Get-UserProfileLastUseTime.ps1 create mode 100644 source/Private/Helpers/New-DirectoryIfNeeded.ps1 create mode 100644 source/Private/Helpers/Resolve-UsernamesToSIDs.ps1 create mode 100644 source/Private/Helpers/Test-EnvironmentVariable.ps1 create mode 100644 source/Private/RemoveProfileReg/Backup-RegistryKeyForSID.ps1 create mode 100644 source/Private/RemoveProfileReg/Confirm-ProfileRemoval.ps1 create mode 100644 source/Private/RemoveProfileReg/Invoke-SingleProfileAction.ps1 create mode 100644 source/Private/RemoveProfileReg/Invoke-UserProfileProcessing.ps1 create mode 100644 source/Private/RemoveProfileReg/Remove-ProfileRegistryKey.ps1 create mode 100644 source/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.ps1 rename source/Private/{ => ValidateFunctions}/Test-FolderExists.ps1 (100%) rename source/Private/{ => ValidateFunctions}/Test-OrphanedProfile.ps1 (100%) rename source/Private/{ => ValidateFunctions}/Test-SpecialAccount.ps1 (100%) rename source/Private/{ => ValidateFunctions}/Validate-SIDFormat.ps1 (100%) rename tests/Unit/Private/{ => ValidateFunctions}/Test-FolderExists.tests.ps1 (100%) rename tests/Unit/Private/{ => ValidateFunctions}/Test-OrphanedProfile.tests.ps1 (100%) rename tests/Unit/Private/{ => ValidateFunctions}/Test-SpecialAccount.tests.ps1 (100%) rename tests/Unit/Private/{ => ValidateFunctions}/Validate-SIDFormat.tests.ps1 (100%) create mode 100644 tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 diff --git a/.vscode/analyzersettings.psd1 b/.vscode/analyzersettings.psd1 index 78312d2..3e8ca86 100644 --- a/.vscode/analyzersettings.psd1 +++ b/.vscode/analyzersettings.psd1 @@ -1,7 +1,7 @@ @{ - CustomRulePath = '.\output\RequiredModules\DscResource.AnalyzerRules' + CustomRulePath = '.\output\RequiredModules\PSScriptAnalyzer' includeDefaultRules = $true - IncludeRules = @( + IncludeRules = @( # DSC Resource Kit style guideline rules. 'PSAvoidDefaultValueForMandatoryParameter', 'PSAvoidDefaultValueSwitchParameter', diff --git a/source/Classes/ProfileDeletionResult.ps1 b/source/Classes/ProfileDeletionResult.ps1 index 3b6d952..824e55e 100644 --- a/source/Classes/ProfileDeletionResult.ps1 +++ b/source/Classes/ProfileDeletionResult.ps1 @@ -1,12 +1,12 @@ class ProfileDeletionResult { [string]$SID - [string]$ProfilePath = $null + [string]$ProfilePath [bool]$DeletionSuccess [string]$DeletionMessage [string]$ComputerName - # Constructor to initialize the properties + # Constructor 1: Full constructor ProfileDeletionResult([string]$sid, [string]$profilePath, [bool]$deletionSuccess, [string]$deletionMessage, [string]$computerName) { $this.SID = $sid @@ -15,4 +15,37 @@ class ProfileDeletionResult $this.DeletionMessage = $deletionMessage $this.ComputerName = $computerName } + + # Constructor 2: Only SID and DeletionSuccess, with default values for others + ProfileDeletionResult([string]$sid, [bool]$deletionSuccess) + { + $this.SID = $sid + $this.ProfilePath = $null + $this.DeletionSuccess = $deletionSuccess + if ($deletionSuccess) + { + $this.DeletionMessage = "Operation successful" + } + else + { + $this.DeletionMessage = "Operation failed" + } + $this.ComputerName = $env:COMPUTERNAME + } + + # Constructor 3: Minimal constructor with defaults for all except SID + ProfileDeletionResult([string]$sid) + { + $this.SID = $sid + $this.ProfilePath = $null + $this.DeletionSuccess = $false + $this.DeletionMessage = "No action performed" + $this.ComputerName = $env:COMPUTERNAME + } + + # Optional method + [string] ToString() + { + return "[$($this.SID)] DeletionSuccess: $($this.DeletionSuccess), Message: $($this.DeletionMessage)" + } } diff --git a/source/Classes/UserProfile.ps1 b/source/Classes/UserProfile.ps1 index 5d981ca..f32f23c 100644 --- a/source/Classes/UserProfile.ps1 +++ b/source/Classes/UserProfile.ps1 @@ -6,6 +6,10 @@ class UserProfile [string]$OrphanReason = $null [string]$ComputerName [bool]$IsSpecial + [string] GetUserNameFromPath() { + return [System.IO.Path]::GetFileName($this.ProfilePath) # Extract the leaf (username) from the ProfilePath + } + # Constructor to initialize the properties UserProfile([string]$sid, [string]$profilePath, [bool]$isOrphaned, [string]$orphanReason, [string]$computerName, [bool]$isSpecial) diff --git a/source/NotImplemented/Get-UserProfileLastUseTime.ps1 b/source/NotImplemented/Get-UserProfileLastUseTime.ps1 new file mode 100644 index 0000000..2d2dcae --- /dev/null +++ b/source/NotImplemented/Get-UserProfileLastUseTime.ps1 @@ -0,0 +1,41 @@ +function Get-UserProfileLastUseTime +{ + [CmdletBinding()] + param ( + [string]$ComputerName = $env:COMPUTERNAME, + [string]$SystemDrive = $env:SystemDrive, + [switch]$UseCitrixLog + ) + + if ($UseCitrixLog) + { + $basePath = "$SystemDrive\Users\*\AppData\Local\Citrix\Receiver\Toaster_.log" + } + else + { + $BasePath = "$SystemDrive\Users\*\AppData\Local\Microsoft\Windows\UsrClass.dat" + + } + + # Check if we are querying a local or remote computer + $isLocal = ($ComputerName -eq $env:COMPUTERNAME) + + # Define user name expression based on whether it's local or remote + if ($isLocal) + { + $UserNameExpression = @{Label = "User"; Expression = { ($_.directory).tostring().split("\")[2] } } + } + else + { + $UserNameExpression = @{Label = "User"; Expression = { ($_.directory).tostring().split("\")[5] } } + } + + # Get the correct directory path (local or remote) + $Path = Get-DirectoryPath -BasePath $BasePath -ComputerName $ComputerName -IsLocal:$isLocal + + # Define a ComputerName column for output + $ComputerNameExpression = @{Label = "ComputerName"; Expression = { $ComputerName } } + + # Retrieve the UsrClass.dat file's last write time for each user profile + Get-ChildItem -Path $Path -Force | Select-Object $UserNameExpression, LastWriteTime, $ComputerNameExpression +} diff --git a/source/Private/Helpers/New-DirectoryIfNeeded.ps1 b/source/Private/Helpers/New-DirectoryIfNeeded.ps1 new file mode 100644 index 0000000..1f8ceb0 --- /dev/null +++ b/source/Private/Helpers/New-DirectoryIfNeeded.ps1 @@ -0,0 +1,23 @@ +function New-DirectoryIfNeeded +{ + param ( + [Parameter(Mandatory = $true)] + [string]$Directory + ) + + try + { + if (-not (Test-Path -Path $Directory)) + { + # Attempt to create the directory if it doesn't exist + $newDirectory = New-Item -Path $Directory -ItemType Directory -Force -ErrorAction Stop + return $newDirectory + } + return $true + } + catch + { + Write-Error "Failed to create directory: $Directory. Error: $_" + return $false + } +} diff --git a/source/Private/Helpers/Resolve-UsernamesToSIDs.ps1 b/source/Private/Helpers/Resolve-UsernamesToSIDs.ps1 new file mode 100644 index 0000000..9f1200f --- /dev/null +++ b/source/Private/Helpers/Resolve-UsernamesToSIDs.ps1 @@ -0,0 +1,22 @@ +function Resolve-UsernamesToSIDs +{ + param ( + [string[]]$Usernames, + [string]$ComputerName + ) + + $SIDs = @() + foreach ($Username in $Usernames) + { + $SID = Get-SIDFromUsername -Username $Username -ComputerName $ComputerName + if ($SID) + { + $SIDs += $SID + } + else + { + Write-Warning "Could not resolve SID for username $Username on $ComputerName." + } + } + return $SIDs +} diff --git a/source/Private/Helpers/Test-EnvironmentVariable.ps1 b/source/Private/Helpers/Test-EnvironmentVariable.ps1 new file mode 100644 index 0000000..3c10a90 --- /dev/null +++ b/source/Private/Helpers/Test-EnvironmentVariable.ps1 @@ -0,0 +1,36 @@ +<# +.SYNOPSIS +Validates the presence of a specific environment variable. + +.DESCRIPTION +The Test-EnvironmentVariable function checks if the specified environment variable exists. +If the variable is found, it returns its value. If not, an error is thrown. + +.PARAMETER Name +The name of the environment variable to check. + +.EXAMPLE +Test-EnvironmentVariable -Name 'Path' + +This command checks if the 'Path' environment variable is present and returns its value if found. + +.OUTPUTS +String (Value of the environment variable) + +.NOTES +This function will throw an error if the environment variable is missing. +#> +function Test-EnvironmentVariable +{ + param ([string]$Name) + + # Dynamically retrieve the environment variable + $value = Get-Item -Path "Env:$Name" -ErrorAction SilentlyContinue + + if (-not $value) + { + throw "Missing required environment variable: $Name" + } + + return $value.Value +} diff --git a/source/Private/Helpers/Update-JsonFile.ps1 b/source/Private/Helpers/Update-JsonFile.ps1 index 3954776..1faf79c 100644 --- a/source/Private/Helpers/Update-JsonFile.ps1 +++ b/source/Private/Helpers/Update-JsonFile.ps1 @@ -23,7 +23,7 @@ function Update-JsonFile $existingData += $RegistryData # Write the updated data back to the file - $existingData | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputFile + $existingData | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputFile -Confirm:$false } else { diff --git a/source/Private/RemoveProfileReg/Backup-RegistryKeyForSID.ps1 b/source/Private/RemoveProfileReg/Backup-RegistryKeyForSID.ps1 new file mode 100644 index 0000000..226186c --- /dev/null +++ b/source/Private/RemoveProfileReg/Backup-RegistryKeyForSID.ps1 @@ -0,0 +1,44 @@ +function Backup-RegistryKeyForSID +{ + param ( + [Parameter(Mandatory = $true)] + [string]$SID, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey]$BaseKey, + + [Parameter(Mandatory = $true)] + [string]$RegBackUpDirectory, + + [Parameter(Mandatory = $true)] + [string]$ComputerName + ) + + try + { + # Ensure the backup directory exists + $directoryCreated = New-DirectoryIfNeeded -Directory $RegBackUpDirectory + + # Check if directory creation failed + if (-not $directoryCreated) + { + Write-Error "Error creating or accessing backup directory: $RegBackUpDirectory" + return $false + } + + # Backup the registry key associated with the SID + $RegBackUpObject = New-RegistryKeyValuesObject -RegistryKey $BaseKey -ComputerName $ComputerName -SubKeyName $SID + $RegBackUpObjectJson = $RegBackUpObject.psobject.copy() + $RegBackUpObjectJson.BackUpDate = $RegBackUpObject.BackUpDate.tostring("o") + + # Update the backup JSON file with the registry data + Update-JsonFile -OutputFile "$RegBackUpDirectory\RegBackUp.json" -RegistryData $RegBackUpObjectJson + + return $true + } + catch + { + Write-Error "Error backing up registry for SID $SID`: $_" + return $false + } +} diff --git a/source/Private/RemoveProfileReg/Confirm-ProfileRemoval.ps1 b/source/Private/RemoveProfileReg/Confirm-ProfileRemoval.ps1 new file mode 100644 index 0000000..f9deeeb --- /dev/null +++ b/source/Private/RemoveProfileReg/Confirm-ProfileRemoval.ps1 @@ -0,0 +1,17 @@ +function Confirm-ProfileRemoval +{ + param ( + [string]$SID, + [Microsoft.Win32.RegistryKey]$BaseKey + ) + + try + { + return ($BaseKey.GetSubKeyNames() -notcontains $SID) + } + catch + { + Write-Error "Error verifying profile removal for SID $SID`: $_" + return $false + } +} diff --git a/source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 b/source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 index 07cebb9..565b219 100644 --- a/source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 +++ b/source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 @@ -1,34 +1,68 @@ +# Main function to process profile removal function Invoke-ProcessProfileRemoval { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [string]$SID, [Microsoft.Win32.RegistryKey]$BaseKey, - [string]$RegBackUpDirectory, [string]$ComputerName, - [object]$SelectedProfile + [UserProfile]$SelectedProfile, # Now expecting a UserProfile object + [switch]$AuditOnly ) try { - # Backup the registry key associated with the SID - $RegBackUpObject = New-RegistryKeyValuesObject -RegistryKey $BaseKey -ComputerName $ComputerName -SubKeyName $SID - Remove-RegistrySubKey -ParentKey $BaseKey -SubKeyName $SID -ThrowOnMissingSubKey $false - - $VerifyDeletion = ($BaseKey.GetSubKeyNames() -notcontains $SID) + # Prepare the properties for the deletion result + $deletionResultParams = @{ + SID = $SelectedProfile.SID + ProfilePath = $SelectedProfile.ProfilePath + ComputerName = $ComputerName + DeletionSuccess = $false + DeletionMessage = "Profile not removed." + } - if ($VerifyDeletion) + if ($AuditOnly) { - Update-JsonFile -OutputFile "$RegBackUpDirectory\RegBackUp.json" -RegistryData $RegBackUpObject - return New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $true -DeletionMessage "Profile removed successfully." -ComputerName $ComputerName + $deletionResultParams.DeletionSuccess = $true + $deletionResultParams.DeletionMessage = "Audit only, no deletion performed." + return New-ProfileDeletionResult @deletionResultParams } - else + + # Get the directory path for backup + $RegBackUpDirectory = Get-DirectoryPath -basePath $env:WinProfileOps_RegBackUpDirectory -ComputerName $ComputerName -IsLocal ($ComputerName -eq $env:COMPUTERNAME) + + if ($PSCmdlet.ShouldProcess("Profile for SID $SID on $ComputerName", "Remove Profile")) { - return New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $false -DeletionMessage "Profile not removed." -ComputerName $ComputerName + # Backup the registry key + if (-not (Backup-RegistryKeyForSID -SID $SID -BaseKey $BaseKey -RegBackUpDirectory $RegBackUpDirectory -ComputerName $ComputerName )) + { + $deletionResultParams.DeletionMessage = "Failed to backup profile." + return New-ProfileDeletionResult @deletionResultParams + } + + # Remove the registry key + if (-not (Remove-ProfileRegistryKey -SID $SID -BaseKey $BaseKey)) + { + $deletionResultParams.DeletionMessage = "Failed to remove profile registry key." + return New-ProfileDeletionResult @deletionResultParams + } + + # Verify the removal + if (Confirm-ProfileRemoval -SID $SID -BaseKey $BaseKey) + { + $deletionResultParams.DeletionSuccess = $true + $deletionResultParams.DeletionMessage = "Profile removed successfully." + } } + + return New-ProfileDeletionResult @deletionResultParams } catch { - Write-Error "Error removing profile for SID $SID`: $_" - return New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $false -DeletionMessage "Error during removal." -ComputerName $ComputerName + Write-Error "Error processing profile removal for SID $SID`: $_" + $deletionResultParams.DeletionMessage = "Error during removal." + return New-ProfileDeletionResult @deletionResultParams } } + +# Helper function to process removal by SID diff --git a/source/Private/RemoveProfileReg/Invoke-SingleProfileAction.ps1 b/source/Private/RemoveProfileReg/Invoke-SingleProfileAction.ps1 new file mode 100644 index 0000000..d02c044 --- /dev/null +++ b/source/Private/RemoveProfileReg/Invoke-SingleProfileAction.ps1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS +Processes actions for a specific user profile identified by SID. + +.DESCRIPTION +The Invoke-SingleProfileAction function processes profile actions such as removal for a specific +user profile, using the SID. The function can audit or remove profiles depending on the parameters passed. + +.PARAMETER ComputerName +The name of the computer where the profile resides. + +.PARAMETER SID +The Security Identifier (SID) of the user profile to process. + +.PARAMETER AuditResults +The results of the audit for the user profiles on the computer. + +.PARAMETER SelectedProfile +(Optional) The user profile object if it's already resolved. If not provided, the function will attempt to resolve it. + +.PARAMETER BaseKey +The registry key for the profile in the registry. + +.PARAMETER DeletionResults +A reference to the array that will store the results of the profile removal operation. + +.PARAMETER Force +A switch to bypass confirmation prompts for profile removal. + +.PARAMETER AuditOnly +A switch to only audit the profile without removing it. + +.EXAMPLE +Invoke-SingleProfileAction -SID 'S-1-5-21-1234567890-1' -AuditResults $auditResults -BaseKey $baseKey -DeletionResults ([ref]$results) + +This command processes the profile for the specified SID, auditing or removing it based on the flags passed. + +.OUTPUTS +ProfileDeletionResult object that includes information about the profile processing result. + +.NOTES +This function should be used in scenarios where profiles need to be audited or removed from the registry. +#> + +function Invoke-SingleProfileAction +{ + param ( + [string]$ComputerName, + [string]$SID, + [UserProfile[]]$AuditResults, + [UserProfile]$SelectedProfile = $null, + [Microsoft.Win32.RegistryKey]$BaseKey, + [ref]$DeletionResults, # Pass by reference + [switch]$Force, + [switch]$AuditOnly, + [bool]$Confirm + ) + + # If $SelectedProfile is null, resolve it using Resolve-UserProfileForDeletion + if (-not $SelectedProfile) + { + $SelectedProfile = Resolve-UserProfileForDeletion -SID $SID -AuditResults $AuditResults -ComputerName $ComputerName + } + + if ($SelectedProfile -is [ProfileDeletionResult]) + { + $DeletionResults.Value += $SelectedProfile + } + # If Force is not used, prompt the user with ShouldContinue + elseif ($Force -or $PSCmdlet.ShouldContinue( + "Remove profile for SID $SID on $($SelectedProfile.ComputerName)?", # Query (shorter message) + "Confirm Deletion of Profile for User $($SelectedProfile.GetUserNameFromPath())" # Caption (more detailed message) + )) + { + # Call the actual removal function + $result = Invoke-ProcessProfileRemoval -SID $SID -SelectedProfile $SelectedProfile -BaseKey $BaseKey -AuditOnly:$AuditOnly -ComputerName $ComputerName -confirm:$Confirm + + # Append result to DeletionResults + $DeletionResults.Value += $result + } +} diff --git a/source/Private/RemoveProfileReg/Invoke-UserProfileProcessing.ps1 b/source/Private/RemoveProfileReg/Invoke-UserProfileProcessing.ps1 new file mode 100644 index 0000000..dcdc984 --- /dev/null +++ b/source/Private/RemoveProfileReg/Invoke-UserProfileProcessing.ps1 @@ -0,0 +1,87 @@ +<# +.SYNOPSIS +Processes user profiles for a specific computer, either by SIDs or UserProfile objects. + +.DESCRIPTION +The Invoke-UserProfileProcessing function processes profiles for a given computer. It can handle multiple +profiles, identified by their SIDs or as UserProfile objects. The function interacts with the registry and manages profile removal or auditing. + +.PARAMETER ComputerName +The name of the computer where the profiles reside. + +.PARAMETER SIDs +(Optional) An array of SIDs for the profiles to process. + +.PARAMETER Profiles +(Optional) An array of UserProfile objects to process. + +.PARAMETER RegistryPath +The path to the registry key where the profiles are stored. + +.PARAMETER ProfileFolderPath +The path to the folder where the user profile directories are stored. + +.PARAMETER RegistryHive +The registry hive where the profiles are stored. + +.EXAMPLE +Invoke-UserProfileProcessing -ComputerName 'RemotePC' -SIDs $sids -RegistryPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' + +This command processes the profiles on the remote computer, identified by their SIDs, and interacts with the registry. + +.OUTPUTS +Array of ProfileDeletionResult objects. + +.NOTES +This function is responsible for handling bulk profile processing on a specific computer. +#> +function Invoke-UserProfileProcessing +{ + param ( + [string]$ComputerName, + [string[]]$SIDs = $null, + [UserProfile[]]$Profiles = $null, + [string]$RegistryPath, + [string]$ProfileFolderPath, + [Microsoft.Win32.RegistryHive]$RegistryHive, + [switch]$Force, + [switch]$AuditOnly, + [bool]$Confirm + ) + + $BaseKey = Open-RegistryKey -ComputerName $ComputerName -RegistryHive $RegistryHive -RegistryPath $RegistryPath + if (-not $BaseKey) + { + Write-Error "Failed to open registry key on computer $ComputerName" + return + } + + try + { + $userProfileAudit = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath + + if ($SIDs) + { + foreach ($SID in $SIDs) + { + Invoke-SingleProfileAction -SID $SID -AuditResults $userProfileAudit -ComputerName $ComputerName ` + -BaseKey $BaseKey -Force:$Force -AuditOnly:$AuditOnly ` + -DeletionResults ([ref]$deletionResults) -Confirm:$Confirm + } + } + + if ($Profiles) + { + foreach ($Profile in $Profiles) + { + Invoke-SingleProfileAction -SID $Profile.SID -AuditResults $userProfileAudit -SelectedProfile $Profile -ComputerName $ComputerName ` + -BaseKey $BaseKey -Force:$Force -AuditOnly:$AuditOnly ` + -DeletionResults ([ref]$deletionResults) -Confirm:$Confirm + } + } + } + finally + { + $BaseKey.Dispose() + } +} diff --git a/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 b/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 index 1d3fc23..c888ff0 100644 --- a/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 +++ b/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 @@ -1,21 +1,39 @@ function New-ProfileDeletionResult { + [CmdletBinding(DefaultParameterSetName = 'Minimal')] param ( - [Parameter(Mandatory = $true)] + # Parameter set 1: Full constructor with all parameters + [Parameter(Mandatory = $true, ParameterSetName = 'Full')] + [Parameter(Mandatory = $true, ParameterSetName = 'SuccessOnly')] [string]$SID, - [Parameter(Mandatory = $false)] + [Parameter(ParameterSetName = 'Full')] [string]$ProfilePath = $null, - [Parameter(Mandatory = $true)] - [bool]$DeletionSuccess, + [Parameter(Mandatory = $true, ParameterSetName = 'Full')] + [Parameter(Mandatory = $true, ParameterSetName = 'SuccessOnly')] + [bool]$DeletionSuccess = $false, - [Parameter(Mandatory = $true)] - [string]$DeletionMessage, + [Parameter(ParameterSetName = 'Full')] + [string]$DeletionMessage = $null, - [Parameter(Mandatory = $true)] - [string]$ComputerName + [Parameter(ParameterSetName = 'Full')] + [string]$ComputerName = $env:COMPUTERNAME ) - return [ProfileDeletionResult]::new($SID, $ProfilePath, $DeletionSuccess, $DeletionMessage, $ComputerName) + switch ($PSCmdlet.ParameterSetName) + { + 'Full' + { + return [ProfileDeletionResult]::new($SID, $ProfilePath, $DeletionSuccess, $DeletionMessage, $ComputerName) + } + 'SuccessOnly' + { + return [ProfileDeletionResult]::new($SID, $DeletionSuccess) + } + 'Minimal' + { + return [ProfileDeletionResult]::new($SID) + } + } } diff --git a/source/Private/RemoveProfileReg/Remove-ProfileRegistryKey.ps1 b/source/Private/RemoveProfileReg/Remove-ProfileRegistryKey.ps1 new file mode 100644 index 0000000..1665b89 --- /dev/null +++ b/source/Private/RemoveProfileReg/Remove-ProfileRegistryKey.ps1 @@ -0,0 +1,18 @@ +function Remove-ProfileRegistryKey +{ + param ( + [string]$SID, + [Microsoft.Win32.RegistryKey]$BaseKey + ) + + try + { + Remove-RegistrySubKey -ParentKey $BaseKey -SubKeyName $SID -ThrowOnMissingSubKey $false -Confirm:$false + return $true + } + catch + { + Write-Error "Error removing registry key for SID $SID`: $_" + return $false + } +} diff --git a/source/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.ps1 b/source/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.ps1 new file mode 100644 index 0000000..f032f90 --- /dev/null +++ b/source/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.ps1 @@ -0,0 +1,36 @@ +function Resolve-UserProfileForDeletion +{ + param ( + [Parameter(Mandatory = $true)] + [string]$SID, # The SID to search for + [Parameter(Mandatory = $true)] + [UserProfile[]]$AuditResults, # The audit results + [Parameter(Mandatory = $true)] + [string]$ComputerName # The target computer name + ) + + # Find the corresponding user profile from the audit + $SelectedProfile = $AuditResults | Where-Object { $_.SID -eq $SID } + + # Handle cases where profile is not found + if ($null -eq $SelectedProfile) + { + # Determine if it's an invalid SID or just not found + $message = if (Validate-SIDFormat -SID $SID) + { + "Profile not found" + Write-Warning "Profile not found for SID: $SID on $ComputerName." + } + else + { + "Invalid SID format encountered" + Write-Warning "Invalid SID format encountered: $SID on $ComputerName." + } + + # Return a ProfileDeletionResult if the profile is not found or invalid + return New-ProfileDeletionResult -SID $SID -ProfilePath $null -DeletionSuccess $false -DeletionMessage $message -ComputerName $ComputerName + } + + # If profile is found, return the UserProfile object + return $SelectedProfile +} diff --git a/source/Private/Test-FolderExists.ps1 b/source/Private/ValidateFunctions/Test-FolderExists.ps1 similarity index 100% rename from source/Private/Test-FolderExists.ps1 rename to source/Private/ValidateFunctions/Test-FolderExists.ps1 diff --git a/source/Private/Test-OrphanedProfile.ps1 b/source/Private/ValidateFunctions/Test-OrphanedProfile.ps1 similarity index 100% rename from source/Private/Test-OrphanedProfile.ps1 rename to source/Private/ValidateFunctions/Test-OrphanedProfile.ps1 diff --git a/source/Private/Test-SpecialAccount.ps1 b/source/Private/ValidateFunctions/Test-SpecialAccount.ps1 similarity index 100% rename from source/Private/Test-SpecialAccount.ps1 rename to source/Private/ValidateFunctions/Test-SpecialAccount.ps1 diff --git a/source/Private/Validate-SIDFormat.ps1 b/source/Private/ValidateFunctions/Validate-SIDFormat.ps1 similarity index 100% rename from source/Private/Validate-SIDFormat.ps1 rename to source/Private/ValidateFunctions/Validate-SIDFormat.ps1 diff --git a/source/Public/Remove-UserProfilesFromRegistry.ps1 b/source/Public/Remove-UserProfilesFromRegistry.ps1 index 1c39fa9..274dfc3 100644 --- a/source/Public/Remove-UserProfilesFromRegistry.ps1 +++ b/source/Public/Remove-UserProfilesFromRegistry.ps1 @@ -2,73 +2,62 @@ function Remove-UserProfilesFromRegistry { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( - [Parameter(Mandatory = $true)] - [string[]]$SIDs, # Array of SIDs to be removed + [Parameter(Mandatory = $true, ParameterSetName = "SIDSet")] + [string[]]$SIDs, - [Parameter(Mandatory = $false)] - [string]$ComputerName = $env:COMPUTERNAME, # Target computer + [Parameter(Mandatory = $true, ParameterSetName = "UserNameSet")] + [string[]]$Usernames, - [switch]$AuditOnly # If set, function will only audit and not remove profiles + [Parameter(Mandatory = $true, ParameterSetName = "UserProfileSet")] + [UserProfile[]]$UserProfiles, + + [string]$ComputerName = $env:COMPUTERNAME, + [switch]$AuditOnly, + [switch]$Force ) Begin { - - $RegistryPath = $env:GetSIDProfileInfo_RegistryPath - $ProfileFolderPath = $env:GetSIDProfileInfo_ProfileFolderPath + # Retrieve and validate necessary paths + $RegistryPath = Test-EnvironmentVariable -Name 'GetSIDProfileInfo_RegistryPath' + $ProfileFolderPath = Test-EnvironmentVariable -Name 'GetSIDProfileInfo_ProfileFolderPath' $RegistryHive = $env:GetSIDProfile_RegistryHive - # Validate Registry Path Variables - if (-not $env:GetSIDProfileInfo_RegistryPath -or -not $env:GetSIDProfileInfo_ProfileFolderPath) - { - throw "Missing registry or profile folder path environment variables." - } + $deletionResults = @() - try + # Convert Usernames to SIDs if needed + if ($PSCmdlet.ParameterSetName -eq 'UserNameSet') { - # Set up for registry backup - Get the directory path, and create if it doesn't exist - $RegBackUpDirectory = Get-DirectoryPath -basePath $env:WinProfileOps_RegBackUpDirectory -ComputerName $ComputerName -IsLocal ($ComputerName -eq $env:COMPUTERNAME) - $null = Test-DirectoryExistence -Directory $RegBackUpDirectory - - # Open the registry key and audit user profiles - $BaseKey = Open-RegistryKey -RegistryHive $RegistryHive -RegistryPath $RegistryPath -ComputerName $ComputerName - $userProfileAudit = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial - - if (-not $BaseKey) - { - throw "Failed to open registry key at path: $RegistryPath" - } + $SIDs = Resolve-UsernamesToSIDs -Usernames $Usernames -ComputerName $ComputerName } - catch + + # Group user profiles by computer for UserProfileSet + if ($PSCmdlet.ParameterSetName -eq 'UserProfileSet') { - throw "Error in Begin block: $_" + $profilesByComputer = $UserProfiles | Group-Object -Property ComputerName } - $deletionResults = @() # Initialize results array } Process { - foreach ($SID in $SIDs) + # Invoke processing based on the parameter set + switch ($PSCmdlet.ParameterSetName) { - - $SelectedProfile = $userProfileAudit | Where-Object { $_.SID -eq $SID } - - if ($null -eq $SelectedProfile) + 'UserProfileSet' { - $deletionResults += New-ProfileDeletionResult -SID $SID -ProfilePath $null -DeletionSuccess $false -DeletionMessage "Profile not found." -ComputerName $ComputerName - continue + foreach ($profileGroup in $profilesByComputer) + { + Invoke-UserProfileProcessing -ComputerName $profileGroup.Name -Profiles $profileGroup.Group ` + -RegistryPath $RegistryPath -ProfileFolderPath $ProfileFolderPath -RegistryHive $RegistryHive ` + -Force:$Force -AuditOnly:$AuditOnly -Confirm:$PSCmdlet.MyInvocation.BoundParameters['Confirm'] + } } - - if ($AuditOnly) - { - $deletionResults += New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $true -DeletionMessage "Audit only, no deletion performed." -ComputerName $ComputerName - continue - } - - if ($PSCmdlet.ShouldProcess($SID, "Remove Profile")) + 'SIDSet' { - $deletionResults += Invoke-ProcessProfileRemoval -SID $SID -BaseKey $BaseKey -RegBackUpDirectory $RegBackUpDirectory -ComputerName $ComputerName -selectedProfile $SelectedProfile + Invoke-UserProfileProcessing -ComputerName $ComputerName -SIDs $SIDs ` + -RegistryPath $RegistryPath -ProfileFolderPath $ProfileFolderPath -RegistryHive $RegistryHive ` + -Force:$Force -AuditOnly:$AuditOnly -Confirm:$PSCmdlet.MyInvocation.BoundParameters['Confirm'] } } } diff --git a/tests/Unit/Private/Test-FolderExists.tests.ps1 b/tests/Unit/Private/ValidateFunctions/Test-FolderExists.tests.ps1 similarity index 100% rename from tests/Unit/Private/Test-FolderExists.tests.ps1 rename to tests/Unit/Private/ValidateFunctions/Test-FolderExists.tests.ps1 diff --git a/tests/Unit/Private/Test-OrphanedProfile.tests.ps1 b/tests/Unit/Private/ValidateFunctions/Test-OrphanedProfile.tests.ps1 similarity index 100% rename from tests/Unit/Private/Test-OrphanedProfile.tests.ps1 rename to tests/Unit/Private/ValidateFunctions/Test-OrphanedProfile.tests.ps1 diff --git a/tests/Unit/Private/Test-SpecialAccount.tests.ps1 b/tests/Unit/Private/ValidateFunctions/Test-SpecialAccount.tests.ps1 similarity index 100% rename from tests/Unit/Private/Test-SpecialAccount.tests.ps1 rename to tests/Unit/Private/ValidateFunctions/Test-SpecialAccount.tests.ps1 diff --git a/tests/Unit/Private/Validate-SIDFormat.tests.ps1 b/tests/Unit/Private/ValidateFunctions/Validate-SIDFormat.tests.ps1 similarity index 100% rename from tests/Unit/Private/Validate-SIDFormat.tests.ps1 rename to tests/Unit/Private/ValidateFunctions/Validate-SIDFormat.tests.ps1 diff --git a/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 b/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 new file mode 100644 index 0000000..610b105 --- /dev/null +++ b/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 @@ -0,0 +1,269 @@ +BeforeAll { + $script:dscModuleName = "WinProfileOps" + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName + + # Set up environment variables used in the function + $env:GetSIDProfileInfo_RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + $env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::LocalMachine + $env:WinProfileOps_RegBackUpDirectory = "C:\LHStuff\RegBackUp" + $env:GetSIDProfileInfo_ProfileFolderPath = "$env:SystemDrive\Users" +} + +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-UserProfilesFromRegistry' -Tag 'Public' { + + BeforeEach { + + InModuleScope -scriptblock { + + # Mock necessary functions + Mock Get-DirectoryPath { "C:\LHStuff\RegBackUp" } + Mock Test-DirectoryExistence { $true } + Mock Open-RegistryKey { New-MockObject -Type Microsoft.Win32.RegistryKey } + Mock Invoke-UserProfileAudit { + param($IgnoreSpecial, $computerName) + + $objects = @() + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1003" -ProfilePath "$env:SystemDrive\Users\TestUserSpecial" -IsOrphaned $false -ComputerName $computerName -IsSpecial $true + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1001" -ProfilePath "$env:SystemDrive\Users\TestUser1" -IsOrphaned $false -ComputerName $computerName -IsSpecial $false + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1002" -ProfilePath "$env:SystemDrive\Users\TestUser2" -IsOrphaned $false -ComputerName $computerName -IsSpecial $false + if ($IgnoreSpecial) + { + return $objects | Where-Object { $_.IsSpecial -eq $false } + } + else + { + return $objects + } + } + + Mock Invoke-ProcessProfileRemoval { + param($SID, $computerName) + New-ProfileDeletionResult -SID $SID -ProfilePath "$env:SystemDrive\Users\TestUser" -DeletionSuccess $true -DeletionMessage "Profile removed successfully." -ComputerName $computerName + } + + Mock Invoke-UserProfileProcessing { + param($ComputerName, $SIDs, $Profiles, $RegistryPath, $ProfileFolderPath, $RegistryHive, $Force, $AuditOnly, $Confirm) + # Simulate successful removal of profiles + foreach ($sid in $SIDs) + { + if ($AuditOnly) + { + + return New-ProfileDeletionResult -SID $sid -ProfilePath "$env:SystemDrive\Users\TestUser" -DeletionSuccess $true -DeletionMessage "Audit only, no deletion performed." -ComputerName $ComputerName + + } + + New-ProfileDeletionResult -SID $sid -ProfilePath "$env:SystemDrive\Users\TestUser" -DeletionSuccess $true -DeletionMessage "Profile removed successfully." -ComputerName $ComputerName + } + } + } -ModuleName $Script:dscModuleName + } + + Context 'When profiles are successfully removed' { + It 'Should remove the user profile successfully' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Profile removed successfully." + } + + It 'Should remove multiple user profiles successfully' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001", "S-1-5-21-1234567890-1002") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 2 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Profile removed successfully." + $result[1].DeletionSuccess | Should -Be $true + $result[1].DeletionMessage | Should -Be "Profile removed successfully." + } + + It 'Should only audit the profile without removing it when AuditOnly is set' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -AuditOnly -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Audit only, no deletion performed." + } + } + + Context 'When confirmation is required' { + It 'Should prompt for confirmation before removing profile' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -WhatIf + $result | Should -BeNullOrEmpty + Should -Invoke Invoke-ProcessProfileRemoval -Exactly 0 -Scope It + } + + It 'Should remove profile when confirmation is bypassed' { + # Using -Confirm:$false to bypass confirmation + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + + Should -Invoke Invoke-ProcessProfileRemoval -Exactly 1 -Scope It + } + } +} + +<# + Context 'When profiles are successfully removed' { + It 'Should remove the user profile successfully' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Profile removed successfully." + } + + It 'Should remove multiple user profiles successfully' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001", "S-1-5-21-1234567890-1002") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 2 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Profile removed successfully." + $result[1].DeletionSuccess | Should -Be $true + $result[1].DeletionMessage | Should -Be "Profile removed successfully." + } + + It 'Should only audit the profile without removing it when AuditOnly is set' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -AuditOnly -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Audit only, no deletion performed." + } + + It 'Should skip special profiles' { + # Mock a special profile + + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1003") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $false + $result[0].DeletionMessage | Should -Be "Profile not found." + } + + It 'Should handle profiles that are already removed or not found' { + # Mock no profile found + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1005") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $false + $result[0].DeletionMessage | Should -Be "Profile not found." + } + + + + } + + Context 'When registry or file operations fail' { + It 'Should throw an error when registry key cannot be opened' { + # Mock registry key failure + Mock Open-RegistryKey { $null } -ModuleName $script:dscModuleName + + $message = 'Error in Begin block: Failed to open registry key at path: SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' + + { Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $env:COMPUTERNAME -Confirm:$false } | Should -Throw $message + } + + It 'Should throw an error when backup directory does not exist' { + # Mock directory existence check to fail + Mock Test-DirectoryExistence { throw } -ModuleName $Script:dscModuleName + + $message = 'Error in Begin block: ScriptHalted' + { Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $env:COMPUTERNAME -Confirm:$false } | Should -Throw $message + } + } + + Context 'When confirmation is required' { + It 'Should prompt for confirmation before removing profile' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -whatif + $result | Should -BeNullOrEmpty + Should -Invoke Invoke-ProcessProfileRemoval -Exactly 0 -Scope It + } + + It 'Should remove profile when confirmation is bypassed' { + # Using -Confirm:$false to bypass confirmation + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $true + + Should -Invoke Invoke-ProcessProfileRemoval -Exactly 1 -Scope It + } + } + + Context 'Handling multiple SIDs with mixed outcomes' { + It 'Should handle a mix of profiles where some are successfully removed and some are not found' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001", "S-1-5-21-1234567890-1005") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 2 + + $result[0].DeletionSuccess | Should -Be $true + $result[0].DeletionMessage | Should -Be "Profile removed successfully." + + $result[1].DeletionSuccess | Should -Be $false + $result[1].DeletionMessage | Should -Be "Profile not found." + } + } + + Context 'When removing profiles from a remote computer' { + It 'Should remove profile from remote computer successfully' { + $remoteComputerName = "RemotePC" + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $remoteComputerName -Confirm:$false + $result | Should -HaveCount 1 + $result[0].ComputerName | Should -Be $remoteComputerName + $result[0].DeletionSuccess | Should -Be $true + } + + It 'Should handle failure when connecting to remote computer' { + Mock Open-RegistryKey { throw } + + $remoteComputerName = "RemotePC" + $message = 'Error in Begin block: ScriptHalted' + { Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $remoteComputerName -Confirm:$false } | Should -Throw $message + } + } + Context 'Handling invalid input and SIDs' { + It 'Should throw an error when no SIDs are provided' { + $message = "Cannot bind argument to parameter 'SIDs' because it is an empty array." + { Remove-UserProfilesFromRegistry -SIDs @() -ComputerName $env:COMPUTERNAME -Confirm:$false } | Should -Throw $message + } + + It 'Should return a message for an invalid SID format' { + # Simulate an invalid SID format + $result = Remove-UserProfilesFromRegistry -SIDs @("Invalid-SID") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $false + $result[0].DeletionMessage | Should -Be "Invalid SID format encountered: 'Invalid-SID'." + } + + It 'Should return a profile not found message for a valid but non-existent SID' { + # Mock no profile found for the given valid SID + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1005") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 1 + $result[0].DeletionSuccess | Should -Be $false + $result[0].DeletionMessage | Should -Be "Profile not found." + } + + It 'Should handle multiple SIDs with one invalid and one valid SID' { + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-10015", "Invalid-SID") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result | Should -HaveCount 2 + + # The first SID should be valid but not found + $result[0].SID | Should -Be "S-1-5-21-1234567890-10015" + $result[0].DeletionSuccess | Should -Be $false + $result[0].DeletionMessage | Should -Be "Profile not found." + + # The second SID should be invalid + $result[1].SID | Should -Be "Invalid-SID" + $result[1].DeletionSuccess | Should -Be $false + $result[1].DeletionMessage | Should -Be "Invalid SID format encountered: 'Invalid-SID'." + } + } +} +#> From 36b38855b75cf7bb5e1d4c24a89079c72a612d01 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 11:50:58 -0700 Subject: [PATCH 06/23] add helper function `Get-SIDFromUsername` --- .../Private/Helpers/Get-SIDFromUsername.ps1 | 67 ++++++++ .../Helpers/Get-SIDFromUsername.tests.ps1 | 157 ++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 source/Private/Helpers/Get-SIDFromUsername.ps1 create mode 100644 tests/Unit/Private/Helpers/Get-SIDFromUsername.tests.ps1 diff --git a/source/Private/Helpers/Get-SIDFromUsername.ps1 b/source/Private/Helpers/Get-SIDFromUsername.ps1 new file mode 100644 index 0000000..3309955 --- /dev/null +++ b/source/Private/Helpers/Get-SIDFromUsername.ps1 @@ -0,0 +1,67 @@ +<# +.SYNOPSIS +Retrieves the Security Identifier (SID) for a given username from a specified computer, defaulting to the local computer if no computer name is provided. + +.DESCRIPTION +The `Get-SIDFromUsername` function queries the specified computer using WMI (CIM) to retrieve the SID associated with a given username. If the `ComputerName` parameter is not provided, the function defaults to the local computer. The function uses the `Get-CimInstance` cmdlet to perform the lookup on the remote or local computer. If the user exists and the SID is found, it is returned. If no SID is found or an error occurs, a warning message is displayed, and the function returns `$null`. + +.PARAMETER Username +Specifies the username for which to retrieve the SID. This parameter is mandatory. + +.PARAMETER ComputerName +Specifies the name of the computer where the user account exists. This parameter is optional and defaults to the local computer (`localhost`). You can specify either a local or remote computer. + +.EXAMPLE +Get-SIDFromUsername -Username 'JohnDoe' -ComputerName 'Server01' + +Description: +This command retrieves the SID for the user 'JohnDoe' from the computer 'Server01'. If the user exists on the computer and has a SID, it will be returned; otherwise, a warning will be displayed. + +.EXAMPLE +Get-SIDFromUsername -Username 'LocalAdmin' + +Description: +This command retrieves the SID for the user 'LocalAdmin' from the local computer (localhost) since no `ComputerName` is provided. If the user exists on the local computer and has a SID, it will be returned; otherwise, a warning will be displayed. + +.EXAMPLE +Get-SIDFromUsername -Username 'DomainUser' -ComputerName 'DomainController' + +Description: +This command retrieves the SID for the user 'DomainUser' from the remote computer 'DomainController'. If the user exists on the specified computer and has a SID, it will be returned; otherwise, a warning will be displayed. + +.NOTES +If the `ComputerName` is not provided, it defaults to the local computer. +#> + +function Get-SIDFromUsername +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Username, + + [Parameter(Mandatory = $false)] + [string]$ComputerName = $env:COMPUTERNAME + ) + + try + { + # Query WMI to get the SID for the given username + $userAccount = Get-CimInstance -Class Win32_UserAccount -ComputerName $ComputerName -Filter "Name = '$Username'" + + if ($userAccount -and $userAccount.SID) + { + return $userAccount.SID + } + else + { + Write-Warning "Could not find SID for username $Username on $ComputerName." + return $null + } + } + catch + { + Write-Warning "An error occurred while trying to resolve SID for username $Username on $ComputerName. Error: $_" + return $null + } +} diff --git a/tests/Unit/Private/Helpers/Get-SIDFromUsername.tests.ps1 b/tests/Unit/Private/Helpers/Get-SIDFromUsername.tests.ps1 new file mode 100644 index 0000000..0641b4c --- /dev/null +++ b/tests/Unit/Private/Helpers/Get-SIDFromUsername.tests.ps1 @@ -0,0 +1,157 @@ +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-SIDFromUsername' -Tags "Pivate", "Helpers" { + # Mock the Get-CimInstance cmdlet to simulate different scenarios + + Context 'When the username exists and has a valid SID' { + It 'should return the correct SID' { + + InModuleScope -ScriptBlock { + + $ComputerName = 'Server01' + # Mock Get-CimInstance to return a valid SID + Mock -CommandName Get-CimInstance -MockWith { + @{ + SID = 'S-1-5-21-1234567890-1234567890-1234567890-1001' + } + } + + # Act: Call the function + $result = Get-SIDFromUsername -Username 'JohnDoe' -ComputerName $ComputerName + + # Assert: Verify the result is the correct SID + $result | Should -Be 'S-1-5-21-1234567890-1234567890-1234567890-1001' + } + } + } + + Context 'When the username does not exist' { + It 'should return null and show a warning' { + + InModuleScope -ScriptBlock { + $ComputerName = 'Server01' + + # Mock Get-CimInstance to return null (user not found) + Mock -CommandName Get-CimInstance -MockWith { $null } + + # Mock Write-Warning to capture the warning message + Mock -CommandName Write-Warning + + # Act: Call the function + $result = Get-SIDFromUsername -Username 'NonExistentUser' -ComputerName $ComputerName + + # Assert: The result should be null + $result | Should -BeNullOrEmpty + + # Assert: Verify that the warning message was displayed + Assert-MockCalled -CommandName Write-Warning -Exactly 1 -Scope It + } + } + } + + Context 'When an error occurs while querying' { + It 'should return null and display a warning with error information' { + + InModuleScope -ScriptBlock { + $ComputerName = 'Server01' + + # Mock Get-CimInstance to throw an exception + Mock -CommandName Get-CimInstance -MockWith { throw "WMI query failed" } + + # Mock Write-Warning to capture the warning message + Mock -CommandName Write-Warning + + # Act: Call the function + $result = Get-SIDFromUsername -Username 'JohnDoe' -ComputerName $ComputerName + + # Assert: The result should be null + $result | Should -BeNullOrEmpty + + # Assert: Verify that the warning message was displayed + Assert-MockCalled -CommandName Write-Warning -Exactly 1 -Scope It + + } + } + } + + Context 'When mandatory parameters are missing' { + It 'should throw a missing parameter error for Username' { + + InModuleScope -ScriptBlock { + + $ComputerName = 'Server01' + # Act & Assert: Expecting the function to throw an error + { Get-SIDFromUsername -Username $Null -ComputerName $ComputerName } | Should -Throw + + } + } + + It 'should default to localhost when ComputerName is missing' { + InModuleScope -ScriptBlock { + # Mock Get-CimInstance to return a valid SID when queried with 'localhost' + Mock -CommandName Get-CimInstance -MockWith { + @{ + SID = 'S-1-5-21-1234567890-1234567890-1234567890-1001' + } + } + + # Act: Call the function without providing ComputerName + $result = Get-SIDFromUsername -Username 'JohnDoe' + + # Assert: The result should match the mock SID + $result | Should -Be 'S-1-5-21-1234567890-1234567890-1234567890-1001' + + # Assert: Ensure Get-CimInstance was called with 'localhost' + Assert-MockCalled -CommandName Get-CimInstance -ParameterFilter { $ComputerName -eq $env:COMPUTERNAME } -Scope It + } + } + } + + Context 'When the SID is missing for a user' { + It 'should return null and display a warning' { + + InModuleScope -ScriptBlock { + + + # Mock Get-CimInstance to return an object without SID + Mock -CommandName Get-CimInstance -MockWith { + @{ + SID = $null + } + } + + # Mock Write-Warning to capture the warning message + Mock -CommandName Write-Warning + + $computerName = 'Server01' + + # Act: Call the function + $result = Get-SIDFromUsername -Username 'JohnDoe' -ComputerName $computerName + + # Assert: The result should be null + $result | Should -BeNullOrEmpty + + # Assert: Verify that the warning message was displayed + Assert-MockCalled -CommandName Write-Warning -Exactly 1 -Scope It + + } + } + } +} From 354c57fc0522706639e24ba87c09e974ce1f87d6 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 12:05:15 -0700 Subject: [PATCH 07/23] add helper function `New-DirectoryIfNeeded` --- .../Private/Helpers/New-DirectoryIfNeeded.ps1 | 42 ++++++- .../Helpers/New-DirectoryIfNeeded.tests.ps1 | 118 ++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Private/Helpers/New-DirectoryIfNeeded.tests.ps1 diff --git a/source/Private/Helpers/New-DirectoryIfNeeded.ps1 b/source/Private/Helpers/New-DirectoryIfNeeded.ps1 index 1f8ceb0..9fe1ed1 100644 --- a/source/Private/Helpers/New-DirectoryIfNeeded.ps1 +++ b/source/Private/Helpers/New-DirectoryIfNeeded.ps1 @@ -1,3 +1,35 @@ +<# +.SYNOPSIS +Creates a directory if it does not already exist. + +.DESCRIPTION +The `New-DirectoryIfNeeded` function checks if the specified directory exists. If it doesn't, the function will create the directory and return the created directory object. If the directory already exists, the function returns `$true`. In case of any errors during directory creation, the function returns `$false` and logs the error. + +.PARAMETER Directory +Specifies the full path of the directory to check or create. This parameter is mandatory. If the directory path is `null`, empty, or contains only whitespace, the function throws an error. + +.EXAMPLE +New-DirectoryIfNeeded -Directory 'C:\Temp\NewFolder' + +Description: +This command checks if the directory 'C:\Temp\NewFolder' exists. If it doesn't, the directory will be created. If the directory already exists, the function will return `$true`. + +.EXAMPLE +New-DirectoryIfNeeded -Directory 'D:\Logs' + +Description: +This command checks if the directory 'D:\Logs' exists. If it does not, the function will create the directory. If the directory already exists, it returns `$true`. + +.EXAMPLE +$directory = New-DirectoryIfNeeded -Directory 'C:\Data\Reports' + +Description: +This command attempts to create the directory 'C:\Data\Reports' if it doesn't exist and assigns the result to `$directory`. If successful, `$directory` will contain the created directory object. If the directory already exists, `$true` will be assigned to `$directory`. + +.NOTES +If the directory path is invalid or if an error occurs during the creation process, the function writes an error message and returns `$false`. +#> + function New-DirectoryIfNeeded { param ( @@ -7,12 +39,20 @@ function New-DirectoryIfNeeded try { + # Check if the Directory parameter is null or an empty string + if ([string]::IsNullOrWhiteSpace($Directory)) + { + throw [System.ArgumentException]::new("The 'Directory' parameter cannot be null or empty.") + } + + # If the directory does not exist, attempt to create it if (-not (Test-Path -Path $Directory)) { - # Attempt to create the directory if it doesn't exist $newDirectory = New-Item -Path $Directory -ItemType Directory -Force -ErrorAction Stop return $newDirectory } + + # If the directory exists, return $true return $true } catch diff --git a/tests/Unit/Private/Helpers/New-DirectoryIfNeeded.tests.ps1 b/tests/Unit/Private/Helpers/New-DirectoryIfNeeded.tests.ps1 new file mode 100644 index 0000000..a27ed8c --- /dev/null +++ b/tests/Unit/Private/Helpers/New-DirectoryIfNeeded.tests.ps1 @@ -0,0 +1,118 @@ +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-DirectoryIfNeeded' -Tags "Private", "Helpers" { + # Context: When the directory does not exist + Context 'When the directory does not exist' { + It 'should create the directory and return the created directory object' { + InModuleScope -ScriptBlock { + # Mock Test-Path to return $false (directory does not exist) + Mock -CommandName Test-Path -MockWith { $false } + + # Mock New-Item to simulate directory creation and return a directory object + Mock -CommandName New-Item -MockWith { + @{ + PSIsContainer = $true + Name = 'NewDirectory' + FullName = 'C:\Temp\NewDirectory' + } + } + + # Act: Call the function + $result = New-DirectoryIfNeeded -Directory 'C:\Temp\NewDirectory' + + # Assert: Verify New-Item was called and returned a directory object + $result.PSIsContainer | Should -Be $true + $result.FullName | Should -Be 'C:\Temp\NewDirectory' + + # Assert: Ensure Test-Path and New-Item were called + Assert-MockCalled -CommandName Test-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName New-Item -Exactly 1 -Scope It + } + } + + } + + # Context: When the directory already exists + Context 'When the directory already exists' { + + It 'should return true and not create the directory' { + InModuleScope -ScriptBlock { + # Mock Test-Path to return $true (directory exists) + Mock -CommandName Test-Path -MockWith { $true } + + # Mock New-Item to simulate directory creation + Mock -CommandName New-Item + + + # Act: Call the function + $result = New-DirectoryIfNeeded -Directory 'C:\Temp\ExistingDirectory' + + # Assert: The function should return $true + $result | Should -Be $true + + # Assert: Ensure New-Item was not called since the directory already exists + Assert-MockCalled -CommandName New-Item -Exactly 0 -Scope It + } + } + + } + + # Context: When an error occurs while creating the directory + Context 'When an error occurs during directory creation' { + + It 'should return false and display an error' { + + InModuleScope -ScriptBlock { + + # Mock Test-Path to return $false (directory does not exist) + Mock -CommandName Test-Path -MockWith { $false } + + # Mock New-Item to simulate an error during directory creation + Mock -CommandName New-Item -MockWith { throw "Unable to create directory" } + + # Mock Write-Error to capture the error message + Mock -CommandName Write-Error + + # Act: Call the function + $result = New-DirectoryIfNeeded -Directory 'C:\Temp\NewDirectory' + + # Assert: The function should return $false + $result | Should -Be $false + + # Assert: Verify Write-Error was called + Assert-MockCalled -CommandName Write-Error -Exactly 1 -Scope It + } + } + } + + + # Context: When a mandatory parameter is missing + Context 'When the directory parameter is missing' { + + It 'should throw a missing parameter error' { + InModuleScope -ScriptBlock { + # Act & Assert: Expect the function to throw a missing parameter error + { New-DirectoryIfNeeded -Directory $null } | Should -Throw + { New-DirectoryIfNeeded -Directory "" } | Should -Throw + } + } + } +} From 9eec8b31c2da28d4d28ec9cd74351452fffe0fba Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 12:44:11 -0700 Subject: [PATCH 08/23] add wrappers for ShouldProcess and ShouldContinue --- .../Private/Helpers/ShouldContinueWrapper.ps1 | 44 ++++++++ .../Private/Helpers/ShouldProcessWrapper.ps1 | 56 ++++++++++ .../Helpers/ShouldContinueWrapper.tests.ps1 | 101 ++++++++++++++++++ .../Helpers/ShouldProcessWrapper.tests.ps1 | 95 ++++++++++++++++ 4 files changed, 296 insertions(+) create mode 100644 source/Private/Helpers/ShouldContinueWrapper.ps1 create mode 100644 source/Private/Helpers/ShouldProcessWrapper.ps1 create mode 100644 tests/Unit/Private/Helpers/ShouldContinueWrapper.tests.ps1 create mode 100644 tests/Unit/Private/Helpers/ShouldProcessWrapper.tests.ps1 diff --git a/source/Private/Helpers/ShouldContinueWrapper.ps1 b/source/Private/Helpers/ShouldContinueWrapper.ps1 new file mode 100644 index 0000000..e08a021 --- /dev/null +++ b/source/Private/Helpers/ShouldContinueWrapper.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS +Handles user confirmation prompts using the `ShouldContinue` method. + +.DESCRIPTION +The `ShouldContinueWrapper` function prompts the user to confirm whether they want to proceed with an operation. It uses the `ShouldContinue` method from the execution context to display a message to the user. The function logs whether the user chose to continue or not and returns the result. + +.PARAMETER Context +Specifies the execution context, typically used to invoke the `ShouldContinue` method. + +.PARAMETER QueryMessage +Specifies the message to display to the user asking if they are sure they want to proceed. + +.PARAMETER CaptionMessage +Specifies the caption of the confirmation prompt, providing additional context about the operation. + +.EXAMPLE +$context = Get-ExecutionContext +ShouldContinueWrapper -Context $context -QueryMessage "Are you sure you want to delete these items?" -CaptionMessage "Confirm Deletion" + +Description: +Prompts the user with the message "Are you sure you want to delete these items?" and the caption "Confirm Deletion". The function returns `$true` if the user chooses to continue, otherwise it returns `$false`. + +.NOTES +This function assumes that it is called within an appropriate execution context where `ShouldContinue` can be invoked. +#> +function ShouldContinueWrapper +{ + param ( + [Parameter(Mandatory = $true)] + $Context, + + [Parameter(Mandatory = $true)] + [string]$QueryMessage, + + [Parameter(Mandatory = $true)] + [string]$CaptionMessage + ) + $result = $Context.ShouldContinue($QueryMessage, $CaptionMessage) + + Write-Verbose "User chose to continue: $result" + + return $result +} diff --git a/source/Private/Helpers/ShouldProcessWrapper.ps1 b/source/Private/Helpers/ShouldProcessWrapper.ps1 new file mode 100644 index 0000000..d8fee8d --- /dev/null +++ b/source/Private/Helpers/ShouldProcessWrapper.ps1 @@ -0,0 +1,56 @@ +<# +.SYNOPSIS +Handles the user confirmation for actions using the `ShouldProcess` method. + +.DESCRIPTION +The `ShouldProcessWrapper` function prompts the user to confirm whether they want to proceed with a specified action on a specified target. It uses the `ShouldProcess` method from the execution context, logging the action and the target for verbose output. The function returns the result of the user's decision, allowing the calling function to proceed or halt based on the confirmation. + +.PARAMETER Context +Specifies the execution context, typically used to invoke the `ShouldProcess` method. + +.PARAMETER Target +Specifies the target of the action, such as a computer, file, or registry path, that the user is being asked to confirm. + +.PARAMETER ActionMessage +Specifies the action that will be performed on the target, such as "Deleting", "Modifying", or "Stopping a service." + +.EXAMPLE +$context = Get-ExecutionContext +ShouldProcessWrapper -Context $context -Target "Server01" -ActionMessage "Delete profiles" + +Description: +Prompts the user to confirm if they want to proceed with deleting profiles from "Server01". The function logs the action and the target, then returns `$true` if the user agrees, otherwise returns `$false`. + +.EXAMPLE +ShouldProcessWrapper -Context $context -Target "C:\Temp\File.txt" -ActionMessage "Remove the file" + +Description: +Prompts the user with the message "Remove the file" for the target file "C:\Temp\File.txt". It logs the action and returns the user's response. + +.NOTES +This function is typically used in cmdlets or scripts that support the `ShouldProcess` functionality to allow confirmation before destructive or critical actions. +#> + +function ShouldProcessWrapper +{ + param ( + [Parameter(Mandatory = $true)] + $Context, + + [Parameter(Mandatory = $true)] + [string]$Target, + + [Parameter(Mandatory = $true)] + [string]$ActionMessage + ) + + # Log the action message for verbose output + Write-Verbose "About to perform action: $ActionMessage on $Target" + + # Use the ShouldProcess method from the context + $result = $Context.ShouldProcess($Target, $ActionMessage) + + Write-Verbose "User chose to process: $result" + + return $result +} diff --git a/tests/Unit/Private/Helpers/ShouldContinueWrapper.tests.ps1 b/tests/Unit/Private/Helpers/ShouldContinueWrapper.tests.ps1 new file mode 100644 index 0000000..778285b --- /dev/null +++ b/tests/Unit/Private/Helpers/ShouldContinueWrapper.tests.ps1 @@ -0,0 +1,101 @@ +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 +} + +# Unit Tests for ShouldContinueWrapper +Describe 'ShouldContinueWrapper' { + + # Context: When the user chooses to continue + Context 'When the user chooses to continue' { + It 'should return true' { + InModuleScope -ScriptBlock { + # Create a mock context and add the ShouldContinue method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldContinue -Value { + param($queryMessage, $captionMessage) + return $true + } + + # Act: Call the function + $result = ShouldContinueWrapper -Context $mockContext -QueryMessage "Are you sure?" -CaptionMessage "Confirm Action" + + # Assert: Should return $true + $result | Should -Be $true + } + } + } + + # Context: When the user chooses NOT to continue + Context 'When the user chooses not to continue' { + It 'should return false' { + InModuleScope -ScriptBlock { + # Create a mock context and add the ShouldContinue method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldContinue -Value { + param($queryMessage, $captionMessage) + return $false + } + + # Act: Call the function + $result = ShouldContinueWrapper -Context $mockContext -QueryMessage "Are you sure?" -CaptionMessage "Confirm Action" + + # Assert: Should return $false + $result | Should -Be $false + } + } + } + + # Context: Ensure parameters are passed correctly + Context 'When parameters are passed correctly' { + It 'should pass the QueryMessage and CaptionMessage to ShouldContinue' { + InModuleScope -ScriptBlock { + # Variables to capture the passed parameters + $env:capturedQueryMessage = $null + $env:capturedCaptionMessage = $null + + # Create a mock context and add the ShouldContinue method + $mockContext = New-Object -TypeName PSObject + + # Create a mock context and add the ShouldContinue method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldContinue -Value { + param($queryMessage, $captionMessage) + $env:capturedQueryMessage = $queryMessage + $env:capturedCaptionMessage = $captionMessage + return $true + } + + # Act: Call the function + $result = ShouldContinueWrapper -Context $mockContext -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + + # Assert: Verify that the parameters were passed correctly + $result | Should -Be $true + + # Assert: Verify that the parameters were passed correctly + $env:capturedQueryMessage | Should -Be "Are you sure?" + $env:capturedCaptionMessage | Should -Be "Confirm Deletion" + + # Cleanup: Remove the captured variables + Remove-Item $env:capturedCaptionMessage -Force -ErrorAction SilentlyContinue + Remove-Item $env:capturedQueryMessage -Force -ErrorAction SilentlyContinue + + + } + } + } +} diff --git a/tests/Unit/Private/Helpers/ShouldProcessWrapper.tests.ps1 b/tests/Unit/Private/Helpers/ShouldProcessWrapper.tests.ps1 new file mode 100644 index 0000000..cbefa8f --- /dev/null +++ b/tests/Unit/Private/Helpers/ShouldProcessWrapper.tests.ps1 @@ -0,0 +1,95 @@ +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 +} + +# Unit Tests for ShouldProcessWrapper +Describe 'ShouldProcessWrapper' { + + # Context: When the user chooses to proceed + Context 'When the user chooses to proceed' { + It 'should return true when user confirms action' { + InModuleScope -ScriptBlock { + # Create a mock context and add the ShouldProcess method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldProcess -Value { + param($target, $actionMessage) + return $true + } + + # Act: Call the function + $result = ShouldProcessWrapper -Context $mockContext -Target "Server01" -ActionMessage "Delete profiles" + + # Assert: Should return $true + $result | Should -Be $true + } + } + } + + # Context: When the user chooses NOT to proceed + Context 'When the user chooses not to proceed' { + It 'should return false when user declines action' { + InModuleScope -ScriptBlock { + # Create a mock context and add the ShouldProcess method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldProcess -Value { + param($target, $actionMessage) + return $false + } + + # Act: Call the function + $result = ShouldProcessWrapper -Context $mockContext -Target "Server01" -ActionMessage "Delete profiles" + + # Assert: Should return $false + $result | Should -Be $false + } + } + } + + # Context: Ensure parameters are passed correctly + Context 'When parameters are passed correctly' { + It 'should pass the Target and ActionMessage to ShouldProcess' { + InModuleScope -ScriptBlock { + # Setup environment variables to capture the passed parameters + $env:capturedTarget = $null + $env:capturedActionMessage = $null + + # Create a mock context and add the ShouldProcess method + $mockContext = New-Object -TypeName PSObject + $mockContext | Add-Member -MemberType ScriptMethod -Name ShouldProcess -Value { + param($target, $actionMessage) + $env:capturedTarget = $target + $env:capturedActionMessage = $actionMessage + return $true + } + + # Act: Call the function + $result = ShouldProcessWrapper -Context $mockContext -Target "C:\Temp\File.txt" -ActionMessage "Remove the file" + + # Assert: Should return $true + $result | Should -Be $true + + # Assert: Verify that the parameters were passed correctly + $env:capturedTarget | Should -Be "C:\Temp\File.txt" + $env:capturedActionMessage | Should -Be "Remove the file" + + # Cleanup: Remove the environment variables + Remove-Item Env:capturedTarget, Env:capturedActionMessage -ErrorAction SilentlyContinue + } + } + } +} From d217ab60608f499beab77ab01f72065b2797694e Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 12:48:00 -0700 Subject: [PATCH 09/23] add helper `Test-EnvironmentVariable` --- .../Helpers/Test-EnvironmentVariable.ps1 | 2 +- .../Test-EnvironmentVariable.tests.ps1 | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Private/Helpers/Test-EnvironmentVariable.tests.ps1 diff --git a/source/Private/Helpers/Test-EnvironmentVariable.ps1 b/source/Private/Helpers/Test-EnvironmentVariable.ps1 index 3c10a90..f9fa276 100644 --- a/source/Private/Helpers/Test-EnvironmentVariable.ps1 +++ b/source/Private/Helpers/Test-EnvironmentVariable.ps1 @@ -18,7 +18,7 @@ This command checks if the 'Path' environment variable is present and returns it String (Value of the environment variable) .NOTES -This function will throw an error if the environment variable is missing. +This function will throw an error if the environment variable is missing from sesion. #> function Test-EnvironmentVariable { diff --git a/tests/Unit/Private/Helpers/Test-EnvironmentVariable.tests.ps1 b/tests/Unit/Private/Helpers/Test-EnvironmentVariable.tests.ps1 new file mode 100644 index 0000000..1871a2a --- /dev/null +++ b/tests/Unit/Private/Helpers/Test-EnvironmentVariable.tests.ps1 @@ -0,0 +1,67 @@ +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 +} + +# Unit Tests for Test-EnvironmentVariable +Describe 'Test-EnvironmentVariable' { + + # Context: When the environment variable exists + Context 'When the environment variable exists' { + It 'should return the value of the environment variable' { + InModuleScope -ScriptBlock { + # Mock the environment variable + $env:TestVariable = "TestValue" + + # Act: Call the function + $result = Test-EnvironmentVariable -Name 'TestVariable' + + # Assert: Should return the value of the environment variable + $result | Should -Be "TestValue" + + # Cleanup: Remove the environment variable + Remove-Item Env:TestVariable -ErrorAction SilentlyContinue + } + } + } + + # Context: When the environment variable does not exist + Context 'When the environment variable does not exist' { + It 'should throw an error indicating the missing environment variable' { + InModuleScope -ScriptBlock { + # Ensure the environment variable does not exist + Remove-Item Env:NonExistentVariable -ErrorAction SilentlyContinue + + # Act & Assert: Call the function and expect an error + { Test-EnvironmentVariable -Name 'NonExistentVariable' } | Should -Throw "Missing required environment variable: NonExistentVariable" + } + } + } + + # Context: Ensure correct behavior when checking common environment variables + Context 'When checking common environment variables' { + It 'should return the value of the Path environment variable if present' { + InModuleScope -ScriptBlock { + # Act: Call the function for 'Path' environment variable + $result = Test-EnvironmentVariable -Name 'Path' + + # Assert: Path environment variable should exist and return its value + $result | Should -Not -BeNullOrEmpty + } + } + } +} From ab44037accb8f40697764f18f85592fc8bf6eb40 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 13:53:34 -0700 Subject: [PATCH 10/23] add helper function `Update-JsonFile` --- source/Private/Helpers/Update-JsonFile.ps1 | 51 ++++- .../Private/Helpers/Update-JsonFile.tests.ps1 | 181 ++++++++++++++++++ 2 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 tests/Unit/Private/Helpers/Update-JsonFile.tests.ps1 diff --git a/source/Private/Helpers/Update-JsonFile.ps1 b/source/Private/Helpers/Update-JsonFile.ps1 index 1faf79c..67fba2a 100644 --- a/source/Private/Helpers/Update-JsonFile.ps1 +++ b/source/Private/Helpers/Update-JsonFile.ps1 @@ -1,3 +1,40 @@ +<# +.SYNOPSIS +Updates an existing JSON file with new registry data or creates a new file if one doesn't exist. + +.DESCRIPTION +The `Update-JsonFile` function checks if a specified JSON file exists and either updates it with new registry data or creates a new file. If the file exists, it reads the current data, appends the new registry data, and writes it back to the file. If the file does not exist, it creates a new file with the provided data. The function handles registry data in a generic array format. + +.PARAMETER OutputFile +Specifies the path to the JSON file that should be updated or created. This parameter is mandatory. + +.PARAMETER RegistryData +Specifies the new registry data to add to the JSON file. This should be passed as an array. The function will append this data to any existing data in the file, or it will create a new file with this data if the file doesn't exist. + +.EXAMPLE +$registryData = @( + @{ Name = 'HKEY_LOCAL_MACHINE\Software\TestKey'; Value = 'TestValue1' }, + @{ Name = 'HKEY_LOCAL_MACHINE\Software\AnotherKey'; Value = 'TestValue2' } +) +Update-JsonFile -OutputFile 'C:\Temp\RegistryData.json' -RegistryData $registryData + +Description: +This example updates the file `RegistryData.json` in `C:\Temp` with the provided `$registryData`. If the file doesn't exist, it will be created. + +.EXAMPLE +Update-JsonFile -OutputFile 'C:\Config\Settings.json' -RegistryData @(@{ Name = 'HKEY_CURRENT_USER\Software\MyApp'; Value = 'UserSetting' }) + +Description: +This command appends the new registry data to the `Settings.json` file located in `C:\Config`. If the file doesn't exist, a new file is created with the registry data. + +.OUTPUTS +None. This function writes updated data back to the file specified in the `OutputFile` parameter. + +.NOTES +- The function automatically handles appending new data to existing arrays in the JSON file. +- JSON files are written with a depth of 10 to ensure nested objects are properly serialized. + +#> function Update-JsonFile { param ( @@ -13,17 +50,23 @@ function Update-JsonFile # Get the existing data and convert it from JSON $existingData = Get-Content -Path $OutputFile -Raw | ConvertFrom-Json - # Check if existing data is an array; if not, convert it into an array + # Ensure existing data is an array, wrap in an array if necessary if (-not ($existingData -is [System.Collections.IEnumerable])) { - $existingData = @(, $existingData) + $existingData = @($existingData) + } + + # Ensure the existing data is an array of objects + if ($existingData -isnot [array]) + { + $existingData = @($existingData) } # Concatenate the existing data and the new data - $existingData += $RegistryData + $combinedData = @($existingData + $RegistryData) # Write the updated data back to the file - $existingData | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputFile -Confirm:$false + $combinedData | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputFile -Confirm:$false } else { diff --git a/tests/Unit/Private/Helpers/Update-JsonFile.tests.ps1 b/tests/Unit/Private/Helpers/Update-JsonFile.tests.ps1 new file mode 100644 index 0000000..b9ca796 --- /dev/null +++ b/tests/Unit/Private/Helpers/Update-JsonFile.tests.ps1 @@ -0,0 +1,181 @@ +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 +} + +# Unit Tests for Update-JsonFile +Describe 'Update-JsonFile' -tags "Private", "Helpers" { + + + BeforeEach { + + InModuleScope -ScriptBlock { + # Mock filesystem interaction for the tests + Mock -CommandName Test-Path + Mock -CommandName Get-Content + Mock -CommandName Set-Content + Mock -CommandName Out-File + + } + + } + + + # Context: Ensure existing data is correctly handled + Context 'When existing data is not an array' { + It 'should convert the existing data to an array and append the new data' { + InModuleScope -ScriptBlock { + # Mock the behavior of Test-Path to simulate the file exists + Mock Test-Path -MockWith { return $true } + + # Mock the behavior of Get-Content and ConvertFrom-Json to simulate a single non-array object + Mock Get-Content -MockWith { return '{"Name":"HKEY_LOCAL_MACHINE\\Software\\SingleKey","Value":"SingleValue"}' } + + # New registry data to be appended + $newRegistryData = @( + @{ Name = 'HKEY_LOCAL_MACHINE\Software\TestKey'; Value = 'TestValue' } + ) + + # Act: Call the function + Update-JsonFile -OutputFile 'C:\Temp\RegistryData.json' -RegistryData $newRegistryData + + #Asert: Ensure that Set-Content was called to write the updated data back to the file + Assert-MockCalled Set-Content -Exactly 1 -Scope It + } + } + } + + # Test: Create a new file when it doesn't exist + Context "When the JSON file does not exist" { + It "Should create a new file with the provided data" { + + InModuleScope -ScriptBlock { + + Mock Test-Path { return $false } + Mock Out-File + + $registryData = @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\TestKey'; Value = 'TestValue' }) + $outputFile = 'C:\Temp\RegistryData.json' + + Update-JsonFile -OutputFile $outputFile -RegistryData $registryData + + Assert-MockCalled Out-File -Exactly 1 -Scope It + + } + } + } + + # Test: Append data when the file exists + Context "When the JSON file exists" { + + It "Should append the new data to the existing JSON file" { + InModuleScope -ScriptBlock { + Mock Test-Path { return $true } + Mock Get-Content { '[{"Name":"HKEY_LOCAL_MACHINE\\Software\\ExistingKey", "Value":"ExistingValue"}]' } + Mock Set-Content + $existingData = @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\ExistingKey'; Value = 'ExistingValue' }) + $newData = @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\NewKey'; Value = 'NewValue' }) + $outputFile = 'C:\Temp\RegistryData.json' + + Update-JsonFile -OutputFile $outputFile -RegistryData $newData + + Assert-MockCalled Set-Content -Exactly 1 -Scope It + } + } + } + + # Test: Handle invalid or empty data + Context "When invalid data is passed" { + + It "Should throw an error if the RegistryData is not passed" { + InModuleScope -ScriptBlock { + { Update-JsonFile -OutputFile 'C:\Temp\RegistryData.json' -RegistryData $null } | Should -Throw + } + } + } + + # Test: Handle multiple entries in RegistryData + Context "When multiple entries are passed in RegistryData" { + It "Should append all entries to the existing JSON file" { + InModuleScope -ScriptBlock { + Mock Test-Path { return $true } + Mock Get-Content { '[{"Name":"HKEY_LOCAL_MACHINE\\Software\\ExistingKey","Value":"ExistingValue"}]' } + Mock Set-Content + + $registryData = @( + @{ Name = 'HKEY_LOCAL_MACHINE\Software\NewKey1'; Value = 'NewValue1' }, + @{ Name = 'HKEY_LOCAL_MACHINE\Software\NewKey2'; Value = 'NewValue2' } + ) + $outputFile = 'C:\Temp\RegistryData.json' + + Update-JsonFile -OutputFile $outputFile -RegistryData $registryData + + Assert-MockCalled Set-Content -Exactly 1 -Scope It + } + } + } + + + + Context "Ensure correct JSON depth is used" { + It "Should output JSON with a depth of 10" { + InModuleScope -ScriptBlock { + Mock Test-Path { return $false } + Mock ConvertTo-Json {} + Mock Out-File + + $registryData = @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\TestKey'; Value = 'TestValue' }) + $outputFile = 'C:\Temp\RegistryData.json' + + Update-JsonFile -OutputFile $outputFile -RegistryData $registryData + + Assert-MockCalled ConvertTo-Json -Exactly 1 -Scope It -ParameterFilter { + $Depth -eq 10 + } + } + } + } + + # Test: Handle missing OutputFile parameter + Context "When OutputFile is not provided" { + It "Should throw an error" { + InModuleScope -ScriptBlock { + { Update-JsonFile -RegistryData @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\NewKey'; Value = 'NewValue' }) -OutputFile $Null } | Should -Throw + } + } + } + + # Test: Handle empty JSON file + Context "When the JSON file exists but is empty" { + It "Should create a new array with the provided registry data" { + InModuleScope -ScriptBlock { + Mock Test-Path { return $true } + Mock Get-Content { '' } # Simulate empty file content + Mock Set-Content + + $newRegistryData = @(@{ Name = 'HKEY_LOCAL_MACHINE\Software\NewKey'; Value = 'NewValue' }) + $outputFile = 'C:\Temp\RegistryData.json' + + Update-JsonFile -OutputFile $outputFile -RegistryData $newRegistryData + + Assert-MockCalled Set-Content -Exactly 1 -Scope It + } + } + } + + +} From 15103fdc4c5ec830470cdf4a28078e15af699215 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 14:27:23 -0700 Subject: [PATCH 11/23] add RemoveProvReg function `Backup-RegistryKeyForSID` --- .../Backup-RegistryKeyForSID.ps1 | 32 +++ .../Backup-RegistryKeyForSID.tests.ps1 | 247 ++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 tests/Unit/Private/RemoveProfileReg/Backup-RegistryKeyForSID.tests.ps1 diff --git a/source/Private/RemoveProfileReg/Backup-RegistryKeyForSID.ps1 b/source/Private/RemoveProfileReg/Backup-RegistryKeyForSID.ps1 index 226186c..866b289 100644 --- a/source/Private/RemoveProfileReg/Backup-RegistryKeyForSID.ps1 +++ b/source/Private/RemoveProfileReg/Backup-RegistryKeyForSID.ps1 @@ -1,3 +1,35 @@ +<# +.SYNOPSIS +Backs up a registry key associated with a specific SID to a specified directory. + +.DESCRIPTION +The `Backup-RegistryKeyForSID` function creates a backup of the registry key associated with the provided SID from a remote or local machine. It ensures that the backup directory exists before proceeding, creates a JSON representation of the registry data, and appends the backup to an existing JSON file. + +.PARAMETER SID +Specifies the Security Identifier (SID) for which the registry key backup is created. + +.PARAMETER BaseKey +Specifies the base registry key under which the SID subkey exists. + +.PARAMETER RegBackUpDirectory +Specifies the directory where the registry backup will be saved. + +.PARAMETER ComputerName +Specifies the name of the computer from which the registry key is being backed up. + +.EXAMPLE +Backup-RegistryKeyForSID -SID 'S-1-5-21-...' -BaseKey $RegistryKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + +Description: +Backs up the registry key for the specified SID from Server01 to the 'C:\Backups' directory. + +.OUTPUTS +Boolean indicating success or failure. + +.NOTES +This function relies on helper functions like `New-DirectoryIfNeeded` and `New-RegistryKeyValuesObject` to handle registry operations. +#> + function Backup-RegistryKeyForSID { param ( diff --git a/tests/Unit/Private/RemoveProfileReg/Backup-RegistryKeyForSID.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Backup-RegistryKeyForSID.tests.ps1 new file mode 100644 index 0000000..a2bdf1e --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Backup-RegistryKeyForSID.tests.ps1 @@ -0,0 +1,247 @@ +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 'Backup-RegistryKeyForSID' -Tag 'Private', "RemoveProfileReg" { + + + BeforeEach { + InModuleScope -ScriptBlock { + # Mock dependencies + Mock -CommandName New-DirectoryIfNeeded + Mock -CommandName New-RegistryKeyValuesObject + Mock -CommandName Update-JsonFile + Mock -CommandName Write-Error + } + } + + # Test: Backup success case + Context 'When the backup is successful' { + It 'Should create a backup of the registry key and return $true' { + InModuleScope -ScriptBlock { + # Mock the directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock registry key backup to return a valid object + Mock New-RegistryKeyValuesObject { + return @{ BackUpDate = (Get-Date) } + } + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Assert that the registry key was backed up + Assert-MockCalled Update-JsonFile -Exactly 1 -Scope It + + # Ensure the function returned true + $result | Should -Be $true + } + } + } + + # Test: Backup directory creation failure + Context 'When the backup directory cannot be created' { + It 'Should return $false and write an error' { + InModuleScope -ScriptBlock { + # Mock directory creation to fail + Mock New-DirectoryIfNeeded { return $false } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Ensure it wrote an error message + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returned false + $result | Should -Be $false + } + } + } + + # Test: Registry key backup failure + Context 'When the registry key backup fails' { + It 'Should return $false' { + InModuleScope -ScriptBlock { + # Mock directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock registry key backup to throw an error + Mock New-RegistryKeyValuesObject { throw "Registry key backup failed" } + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returned false + $result | Should -Be $false + } + } + } + + # Test: Exception handling + Context 'When an unexpected error occurs' { + It 'Should catch the exception and return $false' { + InModuleScope -ScriptBlock { + # Mock directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock registry key backup to succeed + Mock New-RegistryKeyValuesObject { return @{ BackUpDate = (Get-Date) } } + + # Mock Update-JsonFile to throw an exception + Mock Update-JsonFile { throw "Unexpected error" } + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returned false + $result | Should -Be $false + } + } + } + + Context 'When using a network path for the backup directory' { + It 'Should successfully create a backup on a network path' { + InModuleScope -ScriptBlock { + # Mock directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock registry key backup to return a valid object + Mock New-RegistryKeyValuesObject { + return @{ BackUpDate = (Get-Date) } + } + + # Call the function with a network path + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory '\\Server01\Backups' -ComputerName 'Server01' + + # Assert that the registry key was backed up + Assert-MockCalled Update-JsonFile -Exactly 1 -Scope It + + # Ensure the function returned true + $result | Should -Be $true + } + } + } + + + Context 'When the registry key data is empty or null' { + It 'Should return $false and write an error' { + InModuleScope -ScriptBlock { + # Mock directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock registry key backup to return null or empty data + Mock New-RegistryKeyValuesObject { return $null } + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returned false + $result | Should -Be $false + } + } + } + + Context 'When the backup directory exists but is not writable' { + It 'Should return $false and write an error' { + InModuleScope -ScriptBlock { + # Mock directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock Update-JsonFile to throw a permission error + Mock Update-JsonFile { throw "Permission denied" } + + # Call the function with mock data + $result = Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returned false + $result | Should -Be $false + } + } + } + + Context 'When an invalid or empty SID is provided' { + It 'Should throw a ParameterBindingValidationException' { + InModuleScope -ScriptBlock { + # Call the function with an empty SID and verify it throws the expected error + { Backup-RegistryKeyForSID -SID '' -BaseKey $null -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' } | Should -Throw + } + } + } + + Context 'When backing up the registry key with New-RegistryKeyValuesObject' { + It 'Should call New-RegistryKeyValuesObject and set BackUpDate in ISO 8601 format' { + InModuleScope -ScriptBlock { + # Mock the directory creation to succeed + Mock New-DirectoryIfNeeded { return $true } + + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock New-RegistryKeyValuesObject to return a mock object with a BackUpDate property + Mock New-RegistryKeyValuesObject { + return [pscustomobject]@{ BackUpDate = (Get-Date) } + } + + # Call the function + Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + + # Verify that New-RegistryKeyValuesObject was called with the correct parameters + Assert-MockCalled New-RegistryKeyValuesObject -Exactly 1 -Scope It -ParameterFilter { + $RegistryKey -eq $BaseKey -and $ComputerName -eq 'Server01' -and $SubKeyName -eq 'S-1-5-21-12345' + } + + # Verify that the BackUpDate was set correctly in ISO 8601 format + $expectedDate = (Get-Date).ToString("o") + Mock Update-JsonFile -MockWith { + param ($OutputFile, $RegistryData) + $RegistryData.BackUpDate | Should -Be $expectedDate + } + + # Call the function again to test the BackupDate + Backup-RegistryKeyForSID -SID 'S-1-5-21-12345' -BaseKey $BaseKey -RegBackUpDirectory 'C:\Backups' -ComputerName 'Server01' + } + } + } + + +} From 9a1852ba277540ea9571ab3078e6ec0224b1833f Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 14:38:34 -0700 Subject: [PATCH 12/23] add RemoveProvReg Function `Confirm-ProfileRemoval` --- .../Confirm-ProfileRemoval.ps1 | 23 ++ .../Confirm-ProfileRemoval.tests.ps1 | 207 ++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 tests/Unit/Private/RemoveProfileReg/Confirm-ProfileRemoval.tests.ps1 diff --git a/source/Private/RemoveProfileReg/Confirm-ProfileRemoval.ps1 b/source/Private/RemoveProfileReg/Confirm-ProfileRemoval.ps1 index f9deeeb..044a6c7 100644 --- a/source/Private/RemoveProfileReg/Confirm-ProfileRemoval.ps1 +++ b/source/Private/RemoveProfileReg/Confirm-ProfileRemoval.ps1 @@ -1,3 +1,26 @@ +<# +.SYNOPSIS +Verifies whether a registry key for a specific SID has been successfully removed. + +.DESCRIPTION +The `Confirm-ProfileRemoval` function checks whether the registry key associated with the specified SID still exists. If the key no longer exists, the function returns `$true`; otherwise, it returns `$false`. + +.PARAMETER SID +Specifies the Security Identifier (SID) whose registry key removal is being confirmed. + +.PARAMETER BaseKey +Specifies the base registry key under which the SID subkey exists. + +.EXAMPLE +Confirm-ProfileRemoval -SID 'S-1-5-21-...' -BaseKey $RegistryKey + +Description: +Checks if the registry key for the specified SID has been successfully removed. + +.OUTPUTS +Boolean indicating whether the registry key was removed. +#> + function Confirm-ProfileRemoval { param ( diff --git a/tests/Unit/Private/RemoveProfileReg/Confirm-ProfileRemoval.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Confirm-ProfileRemoval.tests.ps1 new file mode 100644 index 0000000..aac8674 --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Confirm-ProfileRemoval.tests.ps1 @@ -0,0 +1,207 @@ +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 'Confirm-ProfileRemoval' -Tag 'Private', "RemoveProfileReg" { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mock Write-Error + Mock -CommandName Write-Error + } + } + + # Test: Registry key exists (SID is found in subkeys) + Context 'When the SID exists in the registry subkeys' { + It 'Should return $false' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning an array that contains the SID + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @('S-1-5-21-12345', 'S-1-5-21-67890') } + } + + # Call the function + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $BaseKey + + # Ensure the function returns $false because the SID exists + $result | Should -Be $false + } + } + } + + # Test: Registry key does not exist (SID is not found in subkeys) + Context 'When the SID does not exist in the registry subkeys' { + It 'Should return $true' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning an array that does not contain the SID + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @('S-1-5-21-67890', 'S-1-5-21-99999') } + } + + # Call the function + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $BaseKey + + # Ensure the function returns $true because the SID does not exist + $result | Should -Be $true + } + } + } + + # Test: Exception occurs while accessing registry + Context 'When there is an error accessing the registry' { + It 'Should return $false and write an error' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object that throws an error when GetSubKeyNames() is called + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { throw "Registry access error" } + } + + # Call the function + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $BaseKey + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure the function returns $false due to the exception + $result | Should -Be $false + } + } + } + + # Test: When BaseKey is empty or null + Context 'When BaseKey is null' { + It 'Should return an False and write an error' { + InModuleScope -ScriptBlock { + + Mock Write-Error {} + + # Call the function with a null BaseKey + $Return = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $null + + #Assert Return is false + $Return | Should -Be $false + + # Ensure an error was written + Assert-MockCalled Write-Error -Exactly 1 -Scope It + } + } + } + + # Test: Empty array from GetSubKeyNames (No subkeys) + Context 'When the registry key has no subkeys' { + It 'Should return $true because there are no subkeys' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning an empty array + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @() } + } + + # Call the function + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $BaseKey + + # Ensure the function returns $true because there are no subkeys + $result | Should -Be $true + } + } + } + + # Test: Multiple subkeys but none matching the SID + Context 'When there are multiple subkeys but none match the SID' { + It 'Should return $true' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning several subkeys, but none match the SID + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @('S-1-5-21-67890', 'S-1-5-21-99999', 'S-1-5-21-11111') } + } + + # Call the function + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-12345' -BaseKey $BaseKey + + # Ensure the function returns $true because the SID does not exist among the subkeys + $result | Should -Be $true + } + } + } + + # Test: When the SID is a non-standard or invalid format + Context 'When an invalid or malformed SID is provided' { + It 'Should return $true' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning a list of subkeys + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @('S-1-5-21-67890') } + } + + # Call the function with an invalid SID + $result = Confirm-ProfileRemoval -SID 'INVALID-SID' -BaseKey $BaseKey + + # Ensure the function returns $false since it's an invalid SID + $result | Should -Be $true + } + } + } + + # Test: Case sensitivity for SID matching + Context 'When the SID is provided in a different case' { + It 'Should still return the correct result regardless of case sensitivity' { + InModuleScope -ScriptBlock { + # Create a mock RegistryKey object with GetSubKeyNames() returning a list of subkeys + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return @('S-1-5-21-12345') } + } + + # Call the function with a lower-case SID + $result = Confirm-ProfileRemoval -SID 's-1-5-21-12345' -BaseKey $BaseKey + + # Ensure the function returns $false because the SID exists, regardless of case + $result | Should -Be $false + } + } + } + + # Test: Handle large number of subkeys + Context 'When there is a very large number of subkeys' { + It 'Should return $true or $false depending on SID existence' { + InModuleScope -ScriptBlock { + # Generate a large list of SIDs + $subKeys = 1..100 | ForEach-Object { "S-1-5-21-$_" } + + # Create a mock RegistryKey object with GetSubKeyNames() returning a large array of subkeys + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ + GetSubKeyNames = { return $subKeys } + } + + # Call the function with a SID that does not exist in the large array + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-110' -BaseKey $BaseKey + + # Ensure the function returns $true because the SID does not exist + $result | Should -Be $true + + # Call the function with a SID that exists in the large array + $result = Confirm-ProfileRemoval -SID 'S-1-5-21-99' -BaseKey $BaseKey + + # Ensure the function returns $false because the SID exists + $result | Should -Be $false + } + } + } + + + + +} From e6bdac53104f355c350562fe414fda33eb2e4b48 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 16:29:28 -0700 Subject: [PATCH 13/23] add RemoveProfileReg function `Invoke-UserProfileRegRemoval` --- .../Invoke-UserProfileRegRemoval.ps1 | 153 +++++++++++ .../Invoke-UserProfileRegRemoval.tests.ps1 | 257 ++++++++++++++++++ 2 files changed, 410 insertions(+) create mode 100644 source/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.ps1 create mode 100644 tests/Unit/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.tests.ps1 diff --git a/source/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.ps1 b/source/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.ps1 new file mode 100644 index 0000000..ce45d90 --- /dev/null +++ b/source/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.ps1 @@ -0,0 +1,153 @@ +<# +.SYNOPSIS +Removes user profile registry entries from local or remote computers, with optional confirmation. + +.DESCRIPTION +The `Invoke-UserProfileRegRemoval` function processes user profiles for removal based on Security Identifiers (SIDs). It retrieves profiles from a specified registry path and profile folder, performs an audit, and optionally prompts for confirmation before removal. The `Force` switch can bypass the confirmation prompt, and the `AuditOnly` switch allows auditing without any removal action. + +If the registry key cannot be opened or the audit fails, the function terminates early to prevent further processing. + +.PARAMETER ComputerName +Specifies the name of the computer where the profile removal is executed. This can be a local or remote machine. + +.PARAMETER SID +Specifies the Security Identifier (SID) of the user profile to remove. This parameter accepts pipeline input, allowing multiple SIDs to be processed sequentially. + +.PARAMETER RegistryPath +Specifies the registry path where user profile information is stored. For example, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList`. + +.PARAMETER ProfileFolderPath +Specifies the folder path where user profile data is stored. For example, `C:\Users`. + +.PARAMETER RegistryHive +Specifies the registry hive (e.g., HKLM for HKEY_LOCAL_MACHINE or HKCU for HKEY_CURRENT_USER) under which the profile keys are located. + +.PARAMETER Force +Forces the removal of profiles without prompting for confirmation. When this switch is used, profiles are removed without any user interaction. + +.PARAMETER AuditOnly +Performs an audit without removing any profiles. The audit results are output to the pipeline, and no changes are made to the registry. + +.PARAMETER Confirm +If specified, the user is prompted for confirmation before removing each profile. The prompt is skipped if `Force` or `AuditOnly` switches are used. + +.EXAMPLE +Get-UserProfiles | Invoke-UserProfileRegRemoval -ComputerName 'Server01' -RegistryPath 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' -Force + +Description: +Removes all user profiles from the registry on Server01 without prompting for confirmation, as the `Force` switch is used. + +.EXAMPLE +Get-UserProfiles | Invoke-UserProfileRegRemoval -ComputerName 'Server02' -RegistryPath 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' -AuditOnly + +Description: +Performs an audit of the user profiles on Server02, but does not remove any profiles. The audit results are output to the pipeline. + +.EXAMPLE +'S-1-5-21-12345' | Invoke-UserProfileRegRemoval -ComputerName 'Server03' -RegistryPath 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' + +Description: +Processes the specified SID ('S-1-5-21-12345') for removal on Server03. If `Confirm` is specified, the user is prompted before the profile is removed. + +.NOTES +- This function uses pipeline input to process multiple SIDs. +- The function handles both local and remote computers. +- Errors during registry key access or audit failure result in early termination. +- If special system profiles are detected during the audit, they can be skipped based on the implementation of the audit function. +#> +function Invoke-UserProfileRegRemoval +{ + [CmdletBinding()] + param ( + [string]$ComputerName, + + # Accept pipeline input for each SID + [Parameter(ValueFromPipeline = $true)] + [string]$SID, + + [string]$RegistryPath, + [string]$ProfileFolderPath, + [Microsoft.Win32.RegistryHive]$RegistryHive, + [switch]$Force, + [switch]$AuditOnly, + [bool]$Confirm + ) + + Begin + { + # Initialize a flag to determine if processing should continue + $continueProcessing = $true + + # Perform audit once for the computer + $BaseKey = Open-RegistryKey -ComputerName $ComputerName -RegistryHive $RegistryHive -RegistryPath $RegistryPath + + # Exit early if the registry key cannot be opened + if (-not $BaseKey -or $null -eq $BaseKey) + { + Write-Error "Failed to open registry key on computer $ComputerName" + $continueProcessing = $false # Set the flag to prevent processing + return # Stop the function entirely if BaseKey is null + } + + # Perform the audit once and store the results if BaseKey is valid + if ($continueProcessing) + { + $userProfileAudit = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial + + if (-not $userProfileAudit) + { + Write-Error "Failed to audit user profiles on computer $ComputerName" + $continueProcessing = $false # Set the flag to prevent processing + return # Stop the function entirely if the audit fails + } + } + } + + Process + { + # Only proceed if the flag allows processing + if ($continueProcessing) + { + # Process each SID as it flows through the pipeline + $SelectedProfile = Resolve-UserProfileForDeletion -SID $SID -AuditResults $userProfileAudit -ComputerName $ComputerName + + if ($SelectedProfile -is [ProfileDeletionResult]) + { + # Output the ProfileDeletionResult directly to the pipeline + $SelectedProfile + } + else + { + # Skip confirmation if AuditOnly is used + if (-not $AuditOnly) + { + if (-not $Force -and (ShouldContinueWrapper -Context $PSCmdlet -QueryMessage "Do you want to delete SID $SID from $($SelectedProfile.ComputerName)?" -CaptionMessage "Confirm Deletion")) + { + $result = Remove-UserProfileRegistryEntry -SelectedProfile $SelectedProfile -BaseKey $BaseKey -AuditOnly:$AuditOnly + $result + } + elseif ($Force) + { + $result = Remove-UserProfileRegistryEntry -SelectedProfile $SelectedProfile -BaseKey $BaseKey -AuditOnly:$AuditOnly + $result + } + } + else + { + # Just process without confirmation + $result = Remove-UserProfileRegistryEntry -SelectedProfile $SelectedProfile -BaseKey $BaseKey -AuditOnly:$AuditOnly + $result + } + } + } + } + + End + { + # Clean up resources + if ($BaseKey) + { + $BaseKey.Dispose() + } + } +} diff --git a/tests/Unit/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.tests.ps1 new file mode 100644 index 0000000..78b473d --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.tests.ps1 @@ -0,0 +1,257 @@ +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 'Invoke-UserProfileRegRemoval' -Tags 'Private', 'UserProfileReg' { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mocking necessary dependencies + Mock -CommandName Invoke-UserProfileAudit + Mock -CommandName Remove-ProfileRegistryKey + Mock -CommandName Write-Error + Mock ShouldContinueWrapper { + param($Context, $QueryMessage, $CaptionMessage) + return $true + } + } + } + + Context 'When confirmation is required' { + It 'Should call ShouldContinueWrapper before removing profile' { + InModuleScope -ScriptBlock { + + Mock -CommandName Remove-UserProfileRegistryEntry {} + + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ Close = {} } } + + # Mock profile audit results using New-UserProfileObject + Mock Invoke-UserProfileAudit { + $mockAuditResults = @() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12346' -ProfilePath 'C:\Users\Test2' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + return $mockAuditResults + } + + # Call the function with the Confirm switch + Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' + + # Ensure ShouldContinueWrapper was called + Assert-MockCalled ShouldContinueWrapper -Exactly 1 -Scope It -ParameterFilter { + $QueryMessage -like '*S-1-5-21-12345*' + } + + # Ensure that Remove-UserProfileRegistryEntry was called after confirmation + Assert-MockCalled Remove-UserProfileRegistryEntry -Exactly 1 -Scope It + } + } + } + + # Test: When in audit mode (AuditOnly switch is used) + Context 'When in audit mode' { + It 'Should perform an audit and not remove the profiles' { + InModuleScope -ScriptBlock { + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ Close = {} } } + + Mock Remove-UserProfileRegistryEntry { + param($SelectedProfile, $BaseKey, $AuditOnly) + $deletionResultParams = @{ + SID = $SelectedProfile.SID + ProfilePath = $SelectedProfile.ProfilePath + ComputerName = $SelectedProfile.ComputerName + DeletionSuccess = $false + DeletionMessage = "Profile not removed." + } + + # If in audit mode, output an audit-only result directly to the pipeline and return + if ($AuditOnly) + { + $deletionResultParams.DeletionSuccess = $true + $deletionResultParams.DeletionMessage = "Audit only, no deletion performed." + New-ProfileDeletionResult @deletionResultParams + return # Return to allow pipeline to continue with the next item + } + + } + + # Mock profile audit results using New-UserProfileObject + Mock Invoke-UserProfileAudit { + $mockAuditResults = @() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12346' -ProfilePath 'C:\Users\Test2' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + return $mockAuditResults + } + + # Call the function with AuditOnly switch + $result = Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' -AuditOnly + + # Ensure that Remove-UserProfileRegistryEntry was NOT called + Assert-MockCalled Remove-UserProfileRegistryEntry -Times 1 -Scope It -ParameterFilter { + $AuditOnly -eq $true + } + } + } + } + + # Test: When using Force switch + Context 'When using Force switch' { + It 'Should remove the profile without confirmation' { + InModuleScope -ScriptBlock { + + Mock -CommandName Remove-UserProfileRegistryEntry {} + + + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ Close = {} } } + + # Mock profile audit results using New-UserProfileObject + Mock Invoke-UserProfileAudit { + $mockAuditResults = @() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + return $mockAuditResults + } + + # Call the function with the Force switch + Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' -Force + + # Ensure that ShouldContinueWrapper was not called (no confirmation) + Assert-MockCalled ShouldContinueWrapper -Exactly 0 -Scope It + + # Ensure Remove-UserProfileRegistryEntry was called + Assert-MockCalled Remove-UserProfileRegistryEntry -Exactly 1 -Scope It + } + } + } + + # Test: When registry key cannot be opened + Context 'When registry key cannot be opened' { + It 'Should write an error and exit' { + InModuleScope -ScriptBlock { + + Mock -CommandName Remove-UserProfileRegistryEntry {} + # Mock the registry key opening to fail + Mock Open-RegistryKey { + param($ComputerName, $RegistryHive, $RegistryPath) + $Out = $Null + return $Out + } + + # Call the function + Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' + + # Ensure Write-Error was called + Assert-MockCalled Write-Error -Exactly 1 -Scope It + + # Ensure Remove-UserProfileRegistryEntry was not called + Assert-MockCalled Remove-UserProfileRegistryEntry -Exactly 0 -Scope It + } + } + } + + # Test: When processing multiple SIDs from the pipeline + Context 'When processing multiple SIDs' { + It 'Should handle multiple SIDs from the pipeline' { + InModuleScope -ScriptBlock { + + Mock -CommandName Remove-UserProfileRegistryEntry {} + + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ Close = {} } } + + # Mock profile audit results using New-UserProfileObject + Mock Invoke-UserProfileAudit { + $mockAuditResults = @() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12346' -ProfilePath 'C:\Users\Test2' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + return $mockAuditResults + } + + # Call the function with multiple SIDs from the pipeline + 'S-1-5-21-12345', 'S-1-5-21-12346' | Invoke-UserProfileRegRemoval -ComputerName 'Server01' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' -Force + + # Ensure that Remove-UserProfileRegistryEntry was called for each SID + Assert-MockCalled Remove-UserProfileRegistryEntry -Exactly 2 -Scope It + } + } + } + + # Test: When profile removal fails + Context 'When profile removal fails' { + It 'Should return deletion object with failure message' { + InModuleScope -ScriptBlock { + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" } + + Mock Backup-RegistryKeyForSID { return $true } + + Mock Remove-ProfileRegistryKey { return $false } + + # Mock profile audit results using New-UserProfileObject + Mock Invoke-UserProfileAudit { + $mockAuditResults = @() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + return $mockAuditResults + } + + # Call the function + $result = Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' + + # Ensure an error was written + $result.DeletionMessage | Should -Be "Failed to remove profile registry key." + + } + } + } + + Context 'When a special profile is encountered' { + It 'Should handle the special profile appropriately' { + InModuleScope -ScriptBlock { + + Mock Remove-UserProfileRegistryEntry {} + + # Mock the registry key opening to succeed + Mock Open-RegistryKey { return New-MockObject -Type "Microsoft.Win32.RegistryKey" -Methods @{ Close = {} } } + + # Mock profile audit results with a special profile + Mock Invoke-UserProfileAudit { + param($ignoreSpecial) + + if ($ignoreSpecial) + { + return @() + } + else + { + return @( + New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $true + ) + } + } + + # Call the function with the special profile + Invoke-UserProfileRegRemoval -ComputerName 'Server01' -SID 'S-1-5-21-12345' -RegistryPath 'Path' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' + + # Ensure Remove-UserProfileRegistryEntry was not called for special profile + Assert-MockCalled Remove-UserProfileRegistryEntry -Exactly 0 -Scope It + } + } + } + +} From 18f3020fc62054c7418156f3bb4e41e1a039a1b9 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 16:53:11 -0700 Subject: [PATCH 14/23] add RemoveProfileReg function `New-ProfileDeletionResult` --- .../New-ProfileDeletionResult.ps1 | 49 +++++++++-- .../New-ProfileDeletionResult.tests.ps1 | 87 +++++++++++++++++++ 2 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 tests/Unit/Private/RemoveProfileReg/New-ProfileDeletionResult.tests.ps1 diff --git a/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 b/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 index c888ff0..39fa4fa 100644 --- a/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 +++ b/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 @@ -1,24 +1,57 @@ +<# +.SYNOPSIS +Creates a new `ProfileDeletionResult` object with details of a user profile deletion. + +.DESCRIPTION +The `New-ProfileDeletionResult` function generates a new object representing the outcome of a user profile deletion operation. This object can include details such as the SID, profile path, deletion status, and computer name. + +.PARAMETER SID +Specifies the Security Identifier (SID) of the user profile. + +.PARAMETER ProfilePath +Specifies the path to the user profile that was deleted (optional). + +.PARAMETER DeletionSuccess +Specifies whether the profile deletion was successful. + +.PARAMETER DeletionMessage +Provides a message regarding the profile deletion result. + +.PARAMETER ComputerName +Specifies the name of the computer from which the profile was removed. + +.EXAMPLE +New-ProfileDeletionResult -SID 'S-1-5-21-...' -DeletionSuccess $true -DeletionMessage 'Profile removed successfully.' + +Description: +Creates a `ProfileDeletionResult` object indicating that the profile for the specified SID was successfully removed. + +.OUTPUTS +ProfileDeletionResult object containing the details of the deletion operation. +#> function New-ProfileDeletionResult { [CmdletBinding(DefaultParameterSetName = 'Minimal')] param ( - # Parameter set 1: Full constructor with all parameters + # SID is mandatory in all parameter sets [Parameter(Mandatory = $true, ParameterSetName = 'Full')] [Parameter(Mandatory = $true, ParameterSetName = 'SuccessOnly')] + [Parameter(Mandatory = $true, ParameterSetName = 'Minimal')] [string]$SID, - [Parameter(ParameterSetName = 'Full')] - [string]$ProfilePath = $null, + # Full parameter set properties + [Parameter(Mandatory = $true, ParameterSetName = 'Full')] + [string]$ProfilePath, [Parameter(Mandatory = $true, ParameterSetName = 'Full')] [Parameter(Mandatory = $true, ParameterSetName = 'SuccessOnly')] - [bool]$DeletionSuccess = $false, + [bool]$DeletionSuccess, - [Parameter(ParameterSetName = 'Full')] - [string]$DeletionMessage = $null, + [Parameter(Mandatory = $true, ParameterSetName = 'Full')] + [string]$DeletionMessage, - [Parameter(ParameterSetName = 'Full')] - [string]$ComputerName = $env:COMPUTERNAME + [Parameter(Mandatory = $true, ParameterSetName = 'Full')] + [string]$ComputerName ) switch ($PSCmdlet.ParameterSetName) diff --git a/tests/Unit/Private/RemoveProfileReg/New-ProfileDeletionResult.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/New-ProfileDeletionResult.tests.ps1 new file mode 100644 index 0000000..f08f7b9 --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/New-ProfileDeletionResult.tests.ps1 @@ -0,0 +1,87 @@ +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-ProfileDeletionResult' -Tags 'Private', 'UserProfile' { + + BeforeEach { + # Setup any common prerequisites or mocks + InModuleScope -ScriptBlock { + # Clear any mocks or global variables if needed + } + } + + # Test: Full parameter set + Context 'When creating a full ProfileDeletionResult object' { + It 'Should create a ProfileDeletionResult object with all properties' { + InModuleScope -ScriptBlock { + # Call the function with all parameters + $result = New-ProfileDeletionResult -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -DeletionSuccess $true -DeletionMessage 'Profile removed successfully.' -ComputerName 'Server01' + + # Validate the result object + $result.SID | Should -Be 'S-1-5-21-12345' + $result.ProfilePath | Should -Be 'C:\Users\Test1' + $result.DeletionSuccess | Should -Be $true + $result.DeletionMessage | Should -Be 'Profile removed successfully.' + $result.ComputerName | Should -Be 'Server01' + } + } + } + + # Test: SuccessOnly parameter set + Context 'When creating a ProfileDeletionResult with SuccessOnly parameter set' { + It 'Should create a ProfileDeletionResult object with only SID and DeletionSuccess' { + InModuleScope -ScriptBlock { + # Call the function with SID and DeletionSuccess only + $result = New-ProfileDeletionResult -SID 'S-1-5-21-67890' -DeletionSuccess $false + + # Validate the result object + $result.SID | Should -Be 'S-1-5-21-67890' + $result.DeletionSuccess | Should -Be $false + $result.ProfilePath | Should -BeNullOrEmpty + $result.DeletionMessage | Should -Be 'Operation failed' + $result.ComputerName | Should -Be $env:COMPUTERNAME + } + } + } + + # Test: Minimal parameter set + Context 'When creating a minimal ProfileDeletionResult object' { + It 'Should create a ProfileDeletionResult object with only SID' { + InModuleScope -ScriptBlock { + # Call the function with only SID + $result = New-ProfileDeletionResult -SID 'S-1-5-21-99999' + + # Validate the result object + $result.SID | Should -Be 'S-1-5-21-99999' + $result.DeletionSuccess | Should -Be $false + $result.ProfilePath | Should -BeNullOrEmpty + $result.DeletionMessage | Should -Be 'No action performed' + $result.ComputerName | Should -Be $env:COMPUTERNAME + } + } + } + # Test: Edge case with no SID provided + Context 'When creating a ProfileDeletionResult without SID' { + It 'Should throw an error because SID is mandatory' { + InModuleScope -ScriptBlock { + { New-ProfileDeletionResult -DeletionSuccess $true } | Should -Throw -ErrorId 'AmbiguousParameterSet,New-ProfileDeletionResult' + } + } + } +} From 3bc56bf85bdf33360b29bbd481f051034e0b8843 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 17:06:03 -0700 Subject: [PATCH 15/23] add RemoveProfileReg function 'PromptForConfirmation' --- .../Remove-ProfileRegistryKey.ps1 | 23 ++++ .../PromptForConfirmation.tests.ps1 | 129 ++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 tests/Unit/Private/RemoveProfileReg/PromptForConfirmation.tests.ps1 diff --git a/source/Private/RemoveProfileReg/Remove-ProfileRegistryKey.ps1 b/source/Private/RemoveProfileReg/Remove-ProfileRegistryKey.ps1 index 1665b89..69665d5 100644 --- a/source/Private/RemoveProfileReg/Remove-ProfileRegistryKey.ps1 +++ b/source/Private/RemoveProfileReg/Remove-ProfileRegistryKey.ps1 @@ -1,3 +1,26 @@ +<# +.SYNOPSIS +Removes a registry key associated with a specific SID. + +.DESCRIPTION +The `Remove-ProfileRegistryKey` function deletes the registry key associated with a specified SID. If the operation fails, an error is logged. + +.PARAMETER SID +Specifies the Security Identifier (SID) whose registry key is being removed. + +.PARAMETER BaseKey +Specifies the base registry key under which the SID subkey exists. + +.EXAMPLE +Remove-ProfileRegistryKey -SID 'S-1-5-21-...' -BaseKey $RegistryKey + +Description: +Removes the registry key for the specified SID from the provided base key. + +.OUTPUTS +Boolean indicating whether the registry key was successfully removed. +#> + function Remove-ProfileRegistryKey { param ( diff --git a/tests/Unit/Private/RemoveProfileReg/PromptForConfirmation.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/PromptForConfirmation.tests.ps1 new file mode 100644 index 0000000..3d0ed63 --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/PromptForConfirmation.tests.ps1 @@ -0,0 +1,129 @@ +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 'PromptForConfirmation' -Tags 'Private', 'Helpers' { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mock the ShouldContinueWrapper for testing user confirmation + Mock ShouldContinueWrapper { + param($Context, $QueryMessage, $CaptionMessage) + return $true # Simulate that the user confirms + } + } + } + + # Test: AuditOnly switch skips the confirmation + Context 'When AuditOnly is used' { + It 'Should skip confirmation and return $true' { + InModuleScope -ScriptBlock { + # Call the function with AuditOnly switch + $result = PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -AuditOnly + + # Validate that the result is true and confirmation is skipped + $result | Should -Be $true + + # Ensure ShouldContinueWrapper was not called + Assert-MockCalled ShouldContinueWrapper -Exactly 0 -Scope It + } + } + } + + # Test: Confirm switch prompts for confirmation + Context 'When Confirm is specified' { + It 'Should prompt the user for confirmation' { + InModuleScope -ScriptBlock { + + $mockContext = New-Object -TypeName PSObject + + # Call the function with Confirm switch + $result = PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -Confirm -context $mockContext + + # Validate that the result is true, assuming the user confirms + $result | Should -Be $true + + # Ensure ShouldContinueWrapper was called with the correct parameters + Assert-MockCalled ShouldContinueWrapper -Exactly 1 -Scope It -ParameterFilter { + $QueryMessage -eq "Are you sure you want to delete 5 profiles from Server01's registry?" -and + $CaptionMessage -eq "Confirm Deletion" + } + } + } + } + + # Test: User declines the confirmation + Context 'When user declines the confirmation' { + It 'Should return $false when the user declines the prompt' { + InModuleScope -ScriptBlock { + # Mock ShouldContinueWrapper to simulate user declining + Mock ShouldContinueWrapper { + return $false # Simulate that the user declines + } + + $mockContext = New-Object -TypeName PSObject + + # Call the function with Confirm switch + $result = PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -Confirm -context $mockContext + # Validate that the result is false, since the user declined + $result | Should -Be $false + + # Ensure ShouldContinueWrapper was called + Assert-MockCalled ShouldContinueWrapper -Exactly 1 -Scope It + } + } + } + + # Test: No Confirm or AuditOnly specified, should proceed without prompt + Context 'When neither AuditOnly nor Confirm are specified' { + It 'Should proceed without prompting and return $true' { + InModuleScope -ScriptBlock { + + $mockContext = New-Object -TypeName PSObject + + # Call the function without AuditOnly or Confirm + $result = PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -context $mockContext + + # Validate that the result is true, proceeding without confirmation + $result | Should -Be $true + + # Ensure ShouldContinueWrapper was not called + Assert-MockCalled ShouldContinueWrapper -Exactly 0 -Scope It + } + } + } + + # Test: Confirm is false, should proceed without prompting + Context 'When Confirm is explicitly set to $false' { + It 'Should proceed without prompting and return $true' { + InModuleScope -ScriptBlock { + + $mockContext = New-Object -TypeName PSObject + + # Call the function with Confirm set to false + $result = PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -Confirm:$false -context $mockContext + + # Validate that the result is true, proceeding without confirmation + $result | Should -Be $true + + # Ensure ShouldContinueWrapper was not called + Assert-MockCalled ShouldContinueWrapper -Exactly 0 -Scope It + } + } + } +} From bb41bd55765e312e9d43889869e420afeaf94c7d Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 17:10:24 -0700 Subject: [PATCH 16/23] add RemoveProfileReg function `Resolve-UsernamesToSIDs` --- .../Resolve-UsernamesToSIDs.ps1 | 45 +++++ .../Resolve-UsernamesToSIDs.tests.ps1 | 171 ++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 create mode 100644 tests/Unit/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.tests.ps1 diff --git a/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 b/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 new file mode 100644 index 0000000..e5c116d --- /dev/null +++ b/source/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS +Resolves a list of usernames to their corresponding Security Identifiers (SIDs). + +.DESCRIPTION +The `Resolve-UsernamesToSIDs` function resolves each provided username to its corresponding SID on the specified computer. If a username cannot be resolved, a warning is logged. + +.PARAMETER Usernames +Specifies an array of usernames to resolve to SIDs. + +.PARAMETER ComputerName +Specifies the name of the computer on which to resolve the usernames. + +.EXAMPLE +Resolve-UsernamesToSIDs -Usernames 'user1', 'user2' -ComputerName 'Server01' + +Description: +Resolves the SIDs for 'user1' and 'user2' on Server01. + +.OUTPUTS +Array of SIDs corresponding to the provided usernames. +#> + +function Resolve-UsernamesToSIDs +{ + param ( + [string[]]$Usernames, + [string]$ComputerName + ) + + $SIDs = @() + foreach ($Username in $Usernames) + { + $SID = Get-SIDFromUsername -Username $Username -ComputerName $ComputerName + if ($SID) + { + $SIDs += $SID + } + else + { + Write-Warning "Could not resolve SID for username $Username on $ComputerName." + } + } + return $SIDs +} diff --git a/tests/Unit/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.tests.ps1 new file mode 100644 index 0000000..585bafb --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Resolve-UsernamesToSIDs.tests.ps1 @@ -0,0 +1,171 @@ +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 'Resolve-UsernamesToSIDs' -Tags 'Private', 'RemoveProfileReg' { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mocking the Get-SIDFromUsername function + Mock -CommandName Get-SIDFromUsername + Mock -CommandName Write-Warning + } + } + + # Test: Resolving valid usernames to SIDs + Context 'When all usernames are valid' { + It 'Should return an array of corresponding SIDs' { + InModuleScope -ScriptBlock { + # Mock Get-SIDFromUsername to return valid SIDs + Mock Get-SIDFromUsername { + param($Username, $ComputerName) + switch ($Username) + { + 'user1' + { + return 'S-1-5-21-1001' + } + 'user2' + { + return 'S-1-5-21-1002' + } + } + } + + # Call the function with valid usernames + $result = Resolve-UsernamesToSIDs -Usernames 'user1', 'user2' -ComputerName 'Server01' + + # Validate that the returned array contains the correct SIDs + $result | Should -Be @('S-1-5-21-1001', 'S-1-5-21-1002') + + # Ensure that Write-Warning was not called + Assert-MockCalled Write-Warning -Exactly 0 -Scope It + } + } + } + + # Test: Resolving usernames with some unresolved + Context 'When some usernames cannot be resolved' { + It 'Should return SIDs for valid usernames and log warnings for unresolved ones' { + InModuleScope -ScriptBlock { + # Mock Get-SIDFromUsername to return SIDs for some usernames and $null for others + Mock Get-SIDFromUsername { + param($Username, $ComputerName) + switch ($Username) + { + 'user1' + { + return 'S-1-5-21-1001' + } + 'invalidUser' + { + return $null + } + } + } + + # Call the function with a mix of valid and invalid usernames + $result = Resolve-UsernamesToSIDs -Usernames 'user1', 'invalidUser' -ComputerName 'Server01' + + # Validate that the returned array contains only the valid SID + $result | Should -Be @('S-1-5-21-1001') + + # Ensure that Write-Warning was called for the unresolved username + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Could not resolve SID for username invalidUser on Server01.' + } + } + } + } + + # Test: Resolving no usernames (empty input) + Context 'When no usernames are provided' { + It 'Should return an empty array' { + InModuleScope -ScriptBlock { + # Call the function with an empty array of usernames + $result = Resolve-UsernamesToSIDs -Usernames @() -ComputerName 'Server01' + + # Validate that the result is an empty array + $result | Should -Be @() + + # Ensure that Get-SIDFromUsername and Write-Warning were not called + Assert-MockCalled Get-SIDFromUsername -Exactly 0 -Scope It + Assert-MockCalled Write-Warning -Exactly 0 -Scope It + } + } + } + + # Test: Resolving multiple usernames + Context 'When resolving multiple usernames' { + It 'Should return the correct SIDs for each username' { + InModuleScope -ScriptBlock { + # Mock Get-SIDFromUsername to return SIDs for multiple usernames + Mock Get-SIDFromUsername { + param($Username, $ComputerName) + switch ($Username) + { + 'user1' + { + return 'S-1-5-21-1001' + } + 'user2' + { + return 'S-1-5-21-1002' + } + 'user3' + { + return 'S-1-5-21-1003' + } + } + } + + # Call the function with multiple usernames + $result = Resolve-UsernamesToSIDs -Usernames 'user1', 'user2', 'user3' -ComputerName 'Server01' + + # Validate that the returned array contains the correct SIDs + $result | Should -Be @('S-1-5-21-1001', 'S-1-5-21-1002', 'S-1-5-21-1003') + + # Ensure that Write-Warning was not called + Assert-MockCalled Write-Warning -Exactly 0 -Scope It + } + } + } + + # Test: Unresolved usernames result in warning + Context 'When a username cannot be resolved' { + It 'Should log a warning for unresolved usernames' { + InModuleScope -ScriptBlock { + # Mock Get-SIDFromUsername to return $null for unresolved usernames + Mock Get-SIDFromUsername { + return $null + } + + # Call the function with an unresolved username + $result = Resolve-UsernamesToSIDs -Usernames 'invalidUser' -ComputerName 'Server01' + + # Validate that the result is an empty array + $result | Should -Be @() + + # Ensure that Write-Warning was called with the correct message + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Could not resolve SID for username invalidUser on Server01.' + } + } + } + } +} From c6baa3a2121aab905c905b943d2eadb37229848f Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 17:14:41 -0700 Subject: [PATCH 17/23] Remove Old functions --- .../Helpers/Resolve-UsernamesToSIDs.ps1 | 22 ----- .../Invoke-ProcessProfileRemoval.ps1 | 68 --------------- .../Invoke-SingleProfileAction.ps1 | 81 ----------------- .../Invoke-UserProfileProcessing.ps1 | 87 ------------------- 4 files changed, 258 deletions(-) delete mode 100644 source/Private/Helpers/Resolve-UsernamesToSIDs.ps1 delete mode 100644 source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 delete mode 100644 source/Private/RemoveProfileReg/Invoke-SingleProfileAction.ps1 delete mode 100644 source/Private/RemoveProfileReg/Invoke-UserProfileProcessing.ps1 diff --git a/source/Private/Helpers/Resolve-UsernamesToSIDs.ps1 b/source/Private/Helpers/Resolve-UsernamesToSIDs.ps1 deleted file mode 100644 index 9f1200f..0000000 --- a/source/Private/Helpers/Resolve-UsernamesToSIDs.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -function Resolve-UsernamesToSIDs -{ - param ( - [string[]]$Usernames, - [string]$ComputerName - ) - - $SIDs = @() - foreach ($Username in $Usernames) - { - $SID = Get-SIDFromUsername -Username $Username -ComputerName $ComputerName - if ($SID) - { - $SIDs += $SID - } - else - { - Write-Warning "Could not resolve SID for username $Username on $ComputerName." - } - } - return $SIDs -} diff --git a/source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 b/source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 deleted file mode 100644 index 565b219..0000000 --- a/source/Private/RemoveProfileReg/Invoke-ProcessProfileRemoval.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -# Main function to process profile removal -function Invoke-ProcessProfileRemoval -{ - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] - param ( - [string]$SID, - [Microsoft.Win32.RegistryKey]$BaseKey, - [string]$ComputerName, - [UserProfile]$SelectedProfile, # Now expecting a UserProfile object - [switch]$AuditOnly - ) - - try - { - # Prepare the properties for the deletion result - $deletionResultParams = @{ - SID = $SelectedProfile.SID - ProfilePath = $SelectedProfile.ProfilePath - ComputerName = $ComputerName - DeletionSuccess = $false - DeletionMessage = "Profile not removed." - } - - if ($AuditOnly) - { - $deletionResultParams.DeletionSuccess = $true - $deletionResultParams.DeletionMessage = "Audit only, no deletion performed." - return New-ProfileDeletionResult @deletionResultParams - } - - # Get the directory path for backup - $RegBackUpDirectory = Get-DirectoryPath -basePath $env:WinProfileOps_RegBackUpDirectory -ComputerName $ComputerName -IsLocal ($ComputerName -eq $env:COMPUTERNAME) - - if ($PSCmdlet.ShouldProcess("Profile for SID $SID on $ComputerName", "Remove Profile")) - { - # Backup the registry key - if (-not (Backup-RegistryKeyForSID -SID $SID -BaseKey $BaseKey -RegBackUpDirectory $RegBackUpDirectory -ComputerName $ComputerName )) - { - $deletionResultParams.DeletionMessage = "Failed to backup profile." - return New-ProfileDeletionResult @deletionResultParams - } - - # Remove the registry key - if (-not (Remove-ProfileRegistryKey -SID $SID -BaseKey $BaseKey)) - { - $deletionResultParams.DeletionMessage = "Failed to remove profile registry key." - return New-ProfileDeletionResult @deletionResultParams - } - - # Verify the removal - if (Confirm-ProfileRemoval -SID $SID -BaseKey $BaseKey) - { - $deletionResultParams.DeletionSuccess = $true - $deletionResultParams.DeletionMessage = "Profile removed successfully." - } - } - - return New-ProfileDeletionResult @deletionResultParams - } - catch - { - Write-Error "Error processing profile removal for SID $SID`: $_" - $deletionResultParams.DeletionMessage = "Error during removal." - return New-ProfileDeletionResult @deletionResultParams - } -} - -# Helper function to process removal by SID diff --git a/source/Private/RemoveProfileReg/Invoke-SingleProfileAction.ps1 b/source/Private/RemoveProfileReg/Invoke-SingleProfileAction.ps1 deleted file mode 100644 index d02c044..0000000 --- a/source/Private/RemoveProfileReg/Invoke-SingleProfileAction.ps1 +++ /dev/null @@ -1,81 +0,0 @@ -<# -.SYNOPSIS -Processes actions for a specific user profile identified by SID. - -.DESCRIPTION -The Invoke-SingleProfileAction function processes profile actions such as removal for a specific -user profile, using the SID. The function can audit or remove profiles depending on the parameters passed. - -.PARAMETER ComputerName -The name of the computer where the profile resides. - -.PARAMETER SID -The Security Identifier (SID) of the user profile to process. - -.PARAMETER AuditResults -The results of the audit for the user profiles on the computer. - -.PARAMETER SelectedProfile -(Optional) The user profile object if it's already resolved. If not provided, the function will attempt to resolve it. - -.PARAMETER BaseKey -The registry key for the profile in the registry. - -.PARAMETER DeletionResults -A reference to the array that will store the results of the profile removal operation. - -.PARAMETER Force -A switch to bypass confirmation prompts for profile removal. - -.PARAMETER AuditOnly -A switch to only audit the profile without removing it. - -.EXAMPLE -Invoke-SingleProfileAction -SID 'S-1-5-21-1234567890-1' -AuditResults $auditResults -BaseKey $baseKey -DeletionResults ([ref]$results) - -This command processes the profile for the specified SID, auditing or removing it based on the flags passed. - -.OUTPUTS -ProfileDeletionResult object that includes information about the profile processing result. - -.NOTES -This function should be used in scenarios where profiles need to be audited or removed from the registry. -#> - -function Invoke-SingleProfileAction -{ - param ( - [string]$ComputerName, - [string]$SID, - [UserProfile[]]$AuditResults, - [UserProfile]$SelectedProfile = $null, - [Microsoft.Win32.RegistryKey]$BaseKey, - [ref]$DeletionResults, # Pass by reference - [switch]$Force, - [switch]$AuditOnly, - [bool]$Confirm - ) - - # If $SelectedProfile is null, resolve it using Resolve-UserProfileForDeletion - if (-not $SelectedProfile) - { - $SelectedProfile = Resolve-UserProfileForDeletion -SID $SID -AuditResults $AuditResults -ComputerName $ComputerName - } - - if ($SelectedProfile -is [ProfileDeletionResult]) - { - $DeletionResults.Value += $SelectedProfile - } - # If Force is not used, prompt the user with ShouldContinue - elseif ($Force -or $PSCmdlet.ShouldContinue( - "Remove profile for SID $SID on $($SelectedProfile.ComputerName)?", # Query (shorter message) - "Confirm Deletion of Profile for User $($SelectedProfile.GetUserNameFromPath())" # Caption (more detailed message) - )) - { - # Call the actual removal function - $result = Invoke-ProcessProfileRemoval -SID $SID -SelectedProfile $SelectedProfile -BaseKey $BaseKey -AuditOnly:$AuditOnly -ComputerName $ComputerName -confirm:$Confirm - - # Append result to DeletionResults - $DeletionResults.Value += $result - } -} diff --git a/source/Private/RemoveProfileReg/Invoke-UserProfileProcessing.ps1 b/source/Private/RemoveProfileReg/Invoke-UserProfileProcessing.ps1 deleted file mode 100644 index dcdc984..0000000 --- a/source/Private/RemoveProfileReg/Invoke-UserProfileProcessing.ps1 +++ /dev/null @@ -1,87 +0,0 @@ -<# -.SYNOPSIS -Processes user profiles for a specific computer, either by SIDs or UserProfile objects. - -.DESCRIPTION -The Invoke-UserProfileProcessing function processes profiles for a given computer. It can handle multiple -profiles, identified by their SIDs or as UserProfile objects. The function interacts with the registry and manages profile removal or auditing. - -.PARAMETER ComputerName -The name of the computer where the profiles reside. - -.PARAMETER SIDs -(Optional) An array of SIDs for the profiles to process. - -.PARAMETER Profiles -(Optional) An array of UserProfile objects to process. - -.PARAMETER RegistryPath -The path to the registry key where the profiles are stored. - -.PARAMETER ProfileFolderPath -The path to the folder where the user profile directories are stored. - -.PARAMETER RegistryHive -The registry hive where the profiles are stored. - -.EXAMPLE -Invoke-UserProfileProcessing -ComputerName 'RemotePC' -SIDs $sids -RegistryPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ProfileFolderPath 'C:\Users' -RegistryHive 'LocalMachine' - -This command processes the profiles on the remote computer, identified by their SIDs, and interacts with the registry. - -.OUTPUTS -Array of ProfileDeletionResult objects. - -.NOTES -This function is responsible for handling bulk profile processing on a specific computer. -#> -function Invoke-UserProfileProcessing -{ - param ( - [string]$ComputerName, - [string[]]$SIDs = $null, - [UserProfile[]]$Profiles = $null, - [string]$RegistryPath, - [string]$ProfileFolderPath, - [Microsoft.Win32.RegistryHive]$RegistryHive, - [switch]$Force, - [switch]$AuditOnly, - [bool]$Confirm - ) - - $BaseKey = Open-RegistryKey -ComputerName $ComputerName -RegistryHive $RegistryHive -RegistryPath $RegistryPath - if (-not $BaseKey) - { - Write-Error "Failed to open registry key on computer $ComputerName" - return - } - - try - { - $userProfileAudit = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath - - if ($SIDs) - { - foreach ($SID in $SIDs) - { - Invoke-SingleProfileAction -SID $SID -AuditResults $userProfileAudit -ComputerName $ComputerName ` - -BaseKey $BaseKey -Force:$Force -AuditOnly:$AuditOnly ` - -DeletionResults ([ref]$deletionResults) -Confirm:$Confirm - } - } - - if ($Profiles) - { - foreach ($Profile in $Profiles) - { - Invoke-SingleProfileAction -SID $Profile.SID -AuditResults $userProfileAudit -SelectedProfile $Profile -ComputerName $ComputerName ` - -BaseKey $BaseKey -Force:$Force -AuditOnly:$AuditOnly ` - -DeletionResults ([ref]$deletionResults) -Confirm:$Confirm - } - } - } - finally - { - $BaseKey.Dispose() - } -} From e4345974d959963f179afbdcab88fd786d792cc7 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 17:15:35 -0700 Subject: [PATCH 18/23] add `PromptForConfirmation` --- .../PromptForConfirmation.ps1 | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 source/Private/RemoveProfileReg/PromptForConfirmation.ps1 diff --git a/source/Private/RemoveProfileReg/PromptForConfirmation.ps1 b/source/Private/RemoveProfileReg/PromptForConfirmation.ps1 new file mode 100644 index 0000000..dbf5fae --- /dev/null +++ b/source/Private/RemoveProfileReg/PromptForConfirmation.ps1 @@ -0,0 +1,67 @@ +<# +.SYNOPSIS +Prompts the user for confirmation before proceeding with a deletion operation. + +.DESCRIPTION +The `PromptForConfirmation` function asks the user to confirm before performing a deletion operation on a specified computer's registry. If the `AuditOnly` flag is specified, the prompt is skipped. If `Confirm` is set to `$true`, the function displays a confirmation message with details about the number of items to delete and the target computer. The user response is handled by the `ShouldContinueWrapper` function, which manages the confirmation prompt. + +.PARAMETER ComputerName +Specifies the name of the computer where the deletion operation will take place. + +.PARAMETER ItemCount +Specifies the number of profiles to delete from the computer's registry. This is displayed in the confirmation message. + +.PARAMETER AuditOnly +If this switch is specified, the function will skip the confirmation prompt and proceed without making any changes. This is typically used for audit or dry-run scenarios. + +.PARAMETER Confirm +If this switch is specified, the function will always prompt the user for confirmation before proceeding. + +.PARAMETER context +Specifies the execution context, typically used to access methods like `ShouldContinue` for the confirmation prompt. + +.EXAMPLE +PromptForConfirmation -ComputerName 'Server01' -ItemCount 5 -Confirm + +Description: +Prompts the user to confirm the deletion of 5 profiles from the registry of 'Server01'. If the user confirms, the function returns `$true`; otherwise, it returns `$false`. + +.EXAMPLE +PromptForConfirmation -ComputerName 'Server02' -ItemCount 10 -AuditOnly + +Description: +Skips the confirmation prompt since the `AuditOnly` switch is used, and returns `$true` to proceed with the audit operation. + +.NOTES +The function assumes that `ShouldContinueWrapper` is available to handle the actual confirmation prompt. +#> + +function PromptForConfirmation +{ + param ( + [string]$ComputerName, + [int]$ItemCount, + [switch]$AuditOnly, + [switch]$Confirm, + $context + ) + + # Skip prompt if in AuditOnly mode + if ($AuditOnly) + { + return $true + } + + + # Always prompt unless Force is specified or Confirm is explicitly set to false + if ($Confirm -eq $true) + { + $QueryMessage = "Are you sure you want to delete $ItemCount profiles from $ComputerName's registry?" + $CaptionMessage = "Confirm Deletion" + + # Use the ShouldContinueWrapper to handle the prompt + return (ShouldContinueWrapper -Context $context -QueryMessage $QueryMessage -CaptionMessage $CaptionMessage) + } + + return $true # Proceed if Force is used or if AuditOnly is true +} From 60ba7ceb7ded9e7085af8392c9657f72fac333df Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 17:43:49 -0700 Subject: [PATCH 19/23] add RemoveProfileReg function `Resolve-UserProfileForDeletion` --- .../New-ProfileDeletionResult.ps1 | 4 +- .../Resolve-UserProfileForDeletion.ps1 | 27 ++- .../Resolve-UserProfileForDeletion.tests.ps1 | 162 ++++++++++++++++++ 3 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.tests.ps1 diff --git a/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 b/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 index 39fa4fa..fb222c8 100644 --- a/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 +++ b/source/Private/RemoveProfileReg/New-ProfileDeletionResult.ps1 @@ -40,8 +40,8 @@ function New-ProfileDeletionResult [string]$SID, # Full parameter set properties - [Parameter(Mandatory = $true, ParameterSetName = 'Full')] - [string]$ProfilePath, + [Parameter(Mandatory = $false, ParameterSetName = 'Full')] + [string]$ProfilePath =$null, [Parameter(Mandatory = $true, ParameterSetName = 'Full')] [Parameter(Mandatory = $true, ParameterSetName = 'SuccessOnly')] diff --git a/source/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.ps1 b/source/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.ps1 index f032f90..560b232 100644 --- a/source/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.ps1 +++ b/source/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.ps1 @@ -1,9 +1,34 @@ +<# +.SYNOPSIS +Finds the user profile for a specific SID in an audit result. + +.DESCRIPTION +The `Resolve-UserProfileForDeletion` function searches through audit results to find the profile associated with a given SID. If the profile is not found, a warning is logged, and a `ProfileDeletionResult` is returned indicating failure. + +.PARAMETER SID +Specifies the Security Identifier (SID) of the profile to search for. + +.PARAMETER AuditResults +Specifies the audit results to search for the profile. + +.PARAMETER ComputerName +Specifies the name of the computer where the profile is located. + +.EXAMPLE +Resolve-UserProfileForDeletion -SID 'S-1-5-21-...' -AuditResults $AuditResults -ComputerName 'Server01' + +Description: +Finds the user profile associated with the specified SID in the audit results for Server01. + +.OUTPUTS +UserProfile or ProfileDeletionResult object. +#> function Resolve-UserProfileForDeletion { param ( [Parameter(Mandatory = $true)] [string]$SID, # The SID to search for - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [UserProfile[]]$AuditResults, # The audit results [Parameter(Mandatory = $true)] [string]$ComputerName # The target computer name diff --git a/tests/Unit/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.tests.ps1 new file mode 100644 index 0000000..84e5506 --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Resolve-UserProfileForDeletion.tests.ps1 @@ -0,0 +1,162 @@ +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 'Resolve-UserProfileForDeletion' -Tags 'Private', 'UserProfileReg' { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mock the Validate-SIDFormat and New-ProfileDeletionResult functions + Mock -CommandName Validate-SIDFormat + Mock -CommandName Write-Warning + } + } + + # Test: Successfully finding a profile + Context 'When the profile exists in the audit results' { + It 'Should return the UserProfile object' { + InModuleScope -ScriptBlock { + # Mock audit results with valid UserProfile objects + $mockAuditResults = @() + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-1002' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + + # Call the function with a valid SID + $result = Resolve-UserProfileForDeletion -SID 'S-1-5-21-1001' -AuditResults $mockAuditResults -ComputerName 'Server01' + + # Validate that the correct UserProfile object is returned + $result.SID | Should -Be 'S-1-5-21-1001' + + # Ensure that Write-Warning was not called + Assert-MockCalled Write-Warning -Exactly 0 -Scope It + } + } + } + + # Test: Profile not found, valid SID format + Context 'When the profile is not found but the SID is valid' { + It 'Should return a ProfileDeletionResult indicating failure and log a warning' { + InModuleScope -ScriptBlock { + # Mock audit results with no matching SID + $mockAuditResults = @() + + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-1002' -ProfilePath $Null -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock Validate-SIDFormat to return true (valid SID format) + Mock Validate-SIDFormat { return $true } + + # Call the function with a valid but non-existent SID + $result = Resolve-UserProfileForDeletion -SID 'S-1-5-21-1001' -AuditResults $mockAuditResults -ComputerName 'Server01' + + # Validate that the ProfileDeletionResult indicates failure + $result.SID | Should -Be 'S-1-5-21-1001' + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be 'Profile not found' + + # Ensure that Write-Warning was called with the correct message + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Profile not found for SID: S-1-5-21-1001 on Server01.' + } + } + } + } + + # Test: Invalid SID format + Context 'When the SID format is invalid' { + It 'Should return a ProfileDeletionResult indicating invalid SID and log a warning' { + InModuleScope -ScriptBlock { + # Mock audit results + $mockAuditResults = @() + + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-1002' -ProfilePath $Null -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock Validate-SIDFormat to return false (invalid SID format) + Mock Validate-SIDFormat { return $false } + + + # Call the function with an invalid SID format + $result = Resolve-UserProfileForDeletion -SID 'Invalid-SID' -AuditResults $mockAuditResults -ComputerName 'Server01' + + # Validate that the ProfileDeletionResult indicates failure due to invalid SID + $result.SID | Should -Be 'Invalid-SID' + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be 'Invalid SID format encountered' + + # Ensure that Write-Warning was called with the correct message + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Invalid SID format encountered: Invalid-SID on Server01.' + } + } + } + } + + # Test: No matching profile and invalid SID format + Context 'When the SID format is invalid and profile is not found' { + It 'Should log an appropriate warning and return a failure result' { + InModuleScope -ScriptBlock { + # Mock Validate-SIDFormat to return false + Mock Validate-SIDFormat { return $false } + + $mockAuditResults = @() + + $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-1002' -ProfilePath $Null -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Call the function with an invalid SID + $result = Resolve-UserProfileForDeletion -SID 'Invalid-SID' -AuditResults $mockAuditResults -ComputerName 'Server01' + + # Validate that the ProfileDeletionResult indicates failure + $result.SID | Should -Be 'Invalid-SID' + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be 'Invalid SID format encountered' + + # Ensure that Write-Warning was called with the correct message + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Invalid SID format encountered: Invalid-SID on Server01.' + } + } + } + } + + # Test: No profiles in the audit results + Context 'When there are no profiles in the audit results' { + It 'Should return a ProfileDeletionResult indicating failure and log a warning' { + InModuleScope -ScriptBlock { + # Mock an empty audit results array + $mockAuditResults = @() + + # Mock Validate-SIDFormat to return true + Mock Validate-SIDFormat { return $true } + + + # Call the function with no profiles in the audit results + $result = Resolve-UserProfileForDeletion -SID 'S-1-5-21-1001' -AuditResults $mockAuditResults -ComputerName 'Server01' + + # Validate that the ProfileDeletionResult indicates failure + $result.SID | Should -Be 'S-1-5-21-1001' + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be 'Profile not found' + + # Ensure that Write-Warning was called with the correct message + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { + $Message -eq 'Profile not found for SID: S-1-5-21-1001 on Server01.' + } + } + } + } +} From a1d1015635862f6c088d5dafc41a38ac7498b91c Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 18:12:37 -0700 Subject: [PATCH 20/23] add RemoveUserProfileReg Function `Remove-UserProfileRegistryEntry` --- .vscode/analyzersettings.psd1 | 2 +- .../Remove-UserProfileRegistryEntry.ps1 | 98 ++ .../Remove-UserProfilesFromRegistry.ps1 | 145 ++- .../Remove-UserProfileRegistryEntry.tests.ps1 | 233 +++++ .../Remove-UserProfilesFromRegistry.tests.ps1 | 941 +++++++++++++++++- 5 files changed, 1369 insertions(+), 50 deletions(-) create mode 100644 source/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.ps1 create mode 100644 tests/Unit/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.tests.ps1 diff --git a/.vscode/analyzersettings.psd1 b/.vscode/analyzersettings.psd1 index 3e8ca86..bbf323e 100644 --- a/.vscode/analyzersettings.psd1 +++ b/.vscode/analyzersettings.psd1 @@ -1,5 +1,5 @@ @{ - CustomRulePath = '.\output\RequiredModules\PSScriptAnalyzer' + CustomRulePath = '.\output\RequiredModules\DscResource.AnalyzerRules' includeDefaultRules = $true IncludeRules = @( # DSC Resource Kit style guideline rules. diff --git a/source/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.ps1 b/source/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.ps1 new file mode 100644 index 0000000..020732d --- /dev/null +++ b/source/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS +Removes a user profile registry entry and backs up the registry data before deletion. + +.DESCRIPTION +The `Remove-UserProfileRegistryEntry` function removes a user profile from the Windows registry. Before removal, it backs up the registry data to a specified directory. The function also supports audit mode, where no deletion occurs but an audit log is created. + +.PARAMETER SelectedProfile +Specifies the user profile object representing the profile to be deleted. + +.PARAMETER BaseKey +Specifies the base registry key under which the profile's SID subkey exists. + +.PARAMETER AuditOnly +If specified, the function will only perform an audit and will not delete the registry entry. + +.EXAMPLE +Remove-UserProfileRegistryEntry -SelectedProfile $Profile -BaseKey $RegistryKey -AuditOnly + +Description: +Performs an audit of the profile without deleting it from the registry. + +.OUTPUTS +ProfileDeletionResult object indicating the outcome of the deletion or audit operation. +#> + +function Remove-UserProfileRegistryEntry +{ + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(ValueFromPipelineByPropertyName = $true)] + [UserProfile]$SelectedProfile, + [Microsoft.Win32.RegistryKey]$BaseKey, + [switch]$AuditOnly + ) + + Process + { + # Prepare the deletion result parameters + $deletionResultParams = @{ + SID = $SelectedProfile.SID + ProfilePath = $SelectedProfile.ProfilePath + ComputerName = $SelectedProfile.ComputerName + DeletionSuccess = $false + DeletionMessage = "Profile not removed." + } + + # Check if BaseKey is null + if (-not $BaseKey) + { + $deletionResultParams.DeletionMessage = "Failed: BaseKey is null, cannot remove the profile." + New-ProfileDeletionResult @deletionResultParams + return # Return early to stop further processing + } + + # If in audit mode, output an audit-only result directly to the pipeline and return + if ($AuditOnly) + { + $deletionResultParams.DeletionSuccess = $true + $deletionResultParams.DeletionMessage = "Audit only, no deletion performed." + New-ProfileDeletionResult @deletionResultParams + return # Return to allow pipeline to continue with the next item + } + + # Determine backup directory + $RegBackUpDirectory = Get-DirectoryPath -basePath $env:WinProfileOps_RegBackUpDirectory -ComputerName $SelectedProfile.ComputerName -IsLocal ($SelectedProfile.ComputerName -eq $env:COMPUTERNAME) + + # Backup the registry key, output failure message if backup fails and skip further processing + if (-not (Backup-RegistryKeyForSID -SID $SelectedProfile.SID -BaseKey $BaseKey -RegBackUpDirectory $RegBackUpDirectory -ComputerName $SelectedProfile.ComputerName)) + { + $deletionResultParams.DeletionMessage = "Failed to backup profile." + New-ProfileDeletionResult @deletionResultParams + return # Return to allow pipeline to continue with the next item + } + + # Remove the registry key, output failure message if removal fails + if (-not (Remove-ProfileRegistryKey -SID $SelectedProfile.SID -BaseKey $BaseKey)) + { + $deletionResultParams.DeletionMessage = "Failed to remove profile registry key." + New-ProfileDeletionResult @deletionResultParams + return # Return to allow pipeline to continue with the next item + } + + # Verify the removal and update the result + if (Confirm-ProfileRemoval -SID $SelectedProfile.SID -BaseKey $BaseKey) + { + $deletionResultParams.DeletionSuccess = $true + $deletionResultParams.DeletionMessage = "Profile removed successfully." + } + else + { + $deletionResultParams.DeletionMessage = "Profile removal verification failed." + } + + # Output the final deletion result + New-ProfileDeletionResult @deletionResultParams + } +} diff --git a/source/Public/Remove-UserProfilesFromRegistry.ps1 b/source/Public/Remove-UserProfilesFromRegistry.ps1 index 274dfc3..19b7b42 100644 --- a/source/Public/Remove-UserProfilesFromRegistry.ps1 +++ b/source/Public/Remove-UserProfilesFromRegistry.ps1 @@ -1,69 +1,166 @@ +<# +.SYNOPSIS +Removes user profiles from the Windows registry based on SIDs, Usernames, or UserProfile objects. + +.DESCRIPTION +The Remove-UserProfilesFromRegistry function allows you to remove user profiles from the Windows registry. +It supports three parameter sets: UserProfileSet, SIDSet, and UserNameSet. The function can be used in +audit-only mode, where no actual removal is performed, or in deletion mode where profiles are removed. + +If AuditOnly is specified, the function will simply output the profiles to be removed without actually performing +any deletions. The function can prompt for confirmation before deletion if required, or use the Force switch +to bypass confirmation. + +.PARAMETER UserProfiles +An array of UserProfile objects to remove from the registry. This parameter is mandatory in the "UserProfileSet" +parameter set. UserProfiles should include the necessary information such as SID, ProfilePath, and ComputerName. + +.PARAMETER SIDs +An array of SIDs of user profiles to remove from the registry. This parameter is mandatory in the "SIDSet" +parameter set. + +.PARAMETER Usernames +An array of usernames to resolve into SIDs and remove from the registry. This parameter is mandatory in the +"UserNameSet" parameter set. + +.PARAMETER ComputerName +Specifies the computer name from which the user profiles should be removed. If not provided, it defaults to +the local computer. + +.PARAMETER AuditOnly +When specified, the function only audits the user profiles and does not perform actual deletion. It will output +information about the profiles that would have been removed. + +.PARAMETER Force +Forces the removal of the user profiles without prompting for confirmation. + +.Outputs +ProfileDeletionResult objects that contain information about the deletion results. + +.EXAMPLE +Remove-UserProfilesFromRegistry -SIDs "S-1-5-21-1234567890-1", "S-1-5-21-1234567890-2" + +Removes user profiles associated with the provided SIDs from the registry of the local computer. + +.EXAMPLE +Remove-UserProfilesFromRegistry -Usernames "john.doe", "jane.smith" -ComputerName "SERVER01" -Force + +Removes the profiles associated with the specified usernames on the "SERVER01" machine without prompting for confirmation. + +.EXAMPLE +Remove-UserProfilesFromRegistry -UserProfiles $userProfileList -AuditOnly + +Audits the profiles in the $userProfileList and outputs what would have been removed without performing actual deletions. + +.NOTES +Requires administrative privileges to remove profiles from the registry. + +.LINK +Get-Help about_Registry +Get-Help about_Profiles +#> function Remove-UserProfilesFromRegistry { + [outputType([ProfileDeletionResult])] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( - [Parameter(Mandatory = $true, ParameterSetName = "SIDSet")] + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "UserProfileSet")] + [UserProfile[]]$UserProfiles, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "SIDSet")] [string[]]$SIDs, - [Parameter(Mandatory = $true, ParameterSetName = "UserNameSet")] + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "UserNameSet")] [string[]]$Usernames, - [Parameter(Mandatory = $true, ParameterSetName = "UserProfileSet")] - [UserProfile[]]$UserProfiles, - [string]$ComputerName = $env:COMPUTERNAME, [switch]$AuditOnly, [switch]$Force + # Default confirm behavior to true ) Begin { - # Retrieve and validate necessary paths + # Retrieve necessary environment variables $RegistryPath = Test-EnvironmentVariable -Name 'GetSIDProfileInfo_RegistryPath' $ProfileFolderPath = Test-EnvironmentVariable -Name 'GetSIDProfileInfo_ProfileFolderPath' - $RegistryHive = $env:GetSIDProfile_RegistryHive - - $deletionResults = @() + $RegistryHive = Test-EnvironmentVariable -Name 'GetSIDProfile_RegistryHive' - # Convert Usernames to SIDs if needed + # Resolve SIDs if Usernames are provided if ($PSCmdlet.ParameterSetName -eq 'UserNameSet') { $SIDs = Resolve-UsernamesToSIDs -Usernames $Usernames -ComputerName $ComputerName + + # If no SIDs were resolved, return early + if (-not $SIDs) + { + Write-Error "No SIDs could be resolved for the provided usernames." + return + } } - # Group user profiles by computer for UserProfileSet + # Group UserProfiles by computer name if using UserProfileSet if ($PSCmdlet.ParameterSetName -eq 'UserProfileSet') { $profilesByComputer = $UserProfiles | Group-Object -Property ComputerName } + # Handle confirmation: default behavior should be prompting unless explicitly set to false + $Confirm = if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('Confirm')) + { + $PSCmdlet.MyInvocation.BoundParameters['Confirm'] + } + else + { + $true # Default to true, always prompt unless explicitly overridden + } } - Process { - # Invoke processing based on the parameter set - switch ($PSCmdlet.ParameterSetName) + # Process UserProfileSet - prompt per computer + if ($PSCmdlet.ParameterSetName -eq 'UserProfileSet') { - 'UserProfileSet' + foreach ($profileGroup in $profilesByComputer) { - foreach ($profileGroup in $profilesByComputer) + $thisComputerName = $profileGroup.Name + $SIDs = $profileGroup.Group.GetEnumerator().SID + $profileCount = $profileGroup.Count + + # Call the confirmation prompt and skip this group if the user does not confirm + if (-not (PromptForConfirmation -ComputerName $thisComputerName -ItemCount $profileCount -AuditOnly:$AuditOnly -Context $PSCmdlet -confirm:$Confirm)) { - Invoke-UserProfileProcessing -ComputerName $profileGroup.Name -Profiles $profileGroup.Group ` - -RegistryPath $RegistryPath -ProfileFolderPath $ProfileFolderPath -RegistryHive $RegistryHive ` - -Force:$Force -AuditOnly:$AuditOnly -Confirm:$PSCmdlet.MyInvocation.BoundParameters['Confirm'] + Write-Verbose "User chose not to continue for $thisComputerName, skipping." + continue } + + # Process the profiles for this computer + $SIDs | Invoke-UserProfileRegRemoval -ComputerName $thisComputerName ` + -RegistryPath $RegistryPath -ProfileFolderPath $ProfileFolderPath ` + -RegistryHive $RegistryHive -Force:$Force -AuditOnly:$AuditOnly -Confirm:$Confirm } - 'SIDSet' + } + + # Process SIDSet and UserNameSet - prompt once for the given computer name + if ($PSCmdlet.ParameterSetName -eq 'SIDSet' -or $PSCmdlet.ParameterSetName -eq 'UserNameSet') + { + $itemCount = $SIDs.Count + + # Call the confirmation prompt and stop if the user does not confirm + if (-not (PromptForConfirmation -ComputerName $ComputerName -ItemCount $itemCount -AuditOnly:$AuditOnly -Context $PSCmdlet -confirm:$Confirm)) { - Invoke-UserProfileProcessing -ComputerName $ComputerName -SIDs $SIDs ` - -RegistryPath $RegistryPath -ProfileFolderPath $ProfileFolderPath -RegistryHive $RegistryHive ` - -Force:$Force -AuditOnly:$AuditOnly -Confirm:$PSCmdlet.MyInvocation.BoundParameters['Confirm'] + Write-Verbose "User chose not to continue for $thisComputerName, skipping." + return } + + # Process the SIDs for this computer name + $SIDs | Invoke-UserProfileRegRemoval -ComputerName $ComputerName ` + -RegistryPath $RegistryPath -ProfileFolderPath $ProfileFolderPath ` + -RegistryHive $RegistryHive -Force:$Force -AuditOnly:$AuditOnly -Confirm:$Confirm } } End { - return $deletionResults + # No need to manually return results; PowerShell will output naturally } } diff --git a/tests/Unit/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.tests.ps1 new file mode 100644 index 0000000..166e4e8 --- /dev/null +++ b/tests/Unit/Private/RemoveProfileReg/Remove-UserProfileRegistryEntry.tests.ps1 @@ -0,0 +1,233 @@ +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-UserProfileRegistryEntry' -Tags 'Private', 'UserProfileReg' { + + BeforeEach { + InModuleScope -ScriptBlock { + # Mock functions used in the Remove-UserProfileRegistryEntry function + Mock -CommandName Backup-RegistryKeyForSID + Mock -CommandName Remove-ProfileRegistryKey + Mock -CommandName Confirm-ProfileRemoval + Mock -CommandName New-ProfileDeletionResult + + } + } + + # Test: Audit mode (AuditOnly switch is used) + Context 'When in audit mode' { + It 'Should return a ProfileDeletionResult with audit-only message and success' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + + # Call the function in AuditOnly mode + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey -AuditOnly + + # Ensure the audit result is returned with success and audit message + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq 'Audit only, no deletion performed.' -and + $DeletionSuccess -eq $true + } + } + } + } + + # Test: Backup failure + Context 'When backup of the profile fails' { + It 'Should return a ProfileDeletionResult indicating backup failure' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock Backup-RegistryKeyForSID to return false (backup failure) + Mock Backup-RegistryKeyForSID { return $false } + + # Call the function + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey + + # Ensure the result indicates backup failure + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq 'Failed to backup profile.' -and + $DeletionSuccess -eq $false + } + + # Ensure no attempt was made to remove the profile if the backup failed + Assert-MockCalled Remove-ProfileRegistryKey -Exactly 0 -Scope It + } + } + } + + # Test: Successful profile removal + Context 'When profile removal is successful' { + It 'Should return a ProfileDeletionResult indicating success' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + + # Mock Backup-RegistryKeyForSID and Remove-ProfileRegistryKey to succeed + Mock Backup-RegistryKeyForSID { return $true } + Mock Remove-ProfileRegistryKey { return $true } + + # Mock Confirm-ProfileRemoval to return true (successful removal) + Mock Confirm-ProfileRemoval { return $true } + + # Call the function + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey + + # Ensure the result indicates successful removal + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq 'Profile removed successfully.' -and + $DeletionSuccess -eq $true + } + } + } + } + + # Test: Confirm-ProfileRemoval succeeds after backup and registry removal + Context 'When profile is successfully backed up, removed, and confirmed' { + It 'Should return a ProfileDeletionResult indicating full success' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock Backup-RegistryKeyForSID and Remove-ProfileRegistryKey to succeed + Mock Backup-RegistryKeyForSID { return $true } + Mock Remove-ProfileRegistryKey { return $true } + + # Mock Confirm-ProfileRemoval to succeed + Mock Confirm-ProfileRemoval { return $true } + + # Call the function + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey + + # Ensure the result indicates full success + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq "Profile removed successfully." -and + $DeletionSuccess -eq $true + } + } + } + } + + + # Test: Failed profile removal + Context 'When profile removal fails after a successful backup' { + It 'Should return a ProfileDeletionResult indicating removal failure' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock Backup-RegistryKeyForSID to succeed + Mock Backup-RegistryKeyForSID { return $true } + + # Mock Remove-ProfileRegistryKey to fail + Mock Remove-ProfileRegistryKey { return $false } + + # Call the function + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey + + # Ensure the result indicates profile removal failure + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq 'Failed to remove profile registry key.' -and + $DeletionSuccess -eq $false + } + } + } + } + + # Test: Failed profile removal confirmation + Context 'When profile removal is successful but confirmation fails' { + It 'Should return a ProfileDeletionResult indicating removal failure' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Mock the base registry key + $BaseKey = New-MockObject -Type "Microsoft.Win32.RegistryKey" + + # Mock Backup-RegistryKeyForSID and Remove-ProfileRegistryKey to succeed + Mock Backup-RegistryKeyForSID { return $true } + Mock Remove-ProfileRegistryKey { return $true } + + # Mock Confirm-ProfileRemoval to fail + Mock Confirm-ProfileRemoval { return $false } + + # Call the function + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $BaseKey + + # Ensure the result indicates profile removal failure despite registry key removal + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq "Profile removal verification failed." -and + $DeletionSuccess -eq $false + } + } + } + } + + + # Test: No BaseKey provided + Context 'When no base registry key is provided' { + It 'Should return a ProfileDeletionResult indicating failure due to missing BaseKey' { + InModuleScope -ScriptBlock { + + # Use New-UserProfileObject to mock UserProfile objects + $mockUserProfile = New-UserProfileObject -SID 'S-1-5-21-1001' -ProfilePath "C:\Users\TestUser1" -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false + + # Call the function with a null BaseKey + $result = Remove-UserProfileRegistryEntry -SelectedProfile $mockUserProfile -BaseKey $null + + # Ensure the result indicates failure due to missing BaseKey + Assert-MockCalled New-ProfileDeletionResult -Exactly 1 -Scope It -ParameterFilter { + $DeletionMessage -eq "Failed: BaseKey is null, cannot remove the profile." -and + $DeletionSuccess -eq $false + } + + # Ensure no backup or profile removal was attempted + Assert-MockCalled Backup-RegistryKeyForSID -Exactly 0 -Scope It + Assert-MockCalled Remove-ProfileRegistryKey -Exactly 0 -Scope It + } + } + } + + +} diff --git a/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 b/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 index 610b105..aa7c22a 100644 --- a/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 +++ b/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 @@ -25,6 +25,875 @@ AfterAll { Describe 'Remove-UserProfilesFromRegistry' -Tag 'Public' { + # Mock the environment variables and helper functions + BeforeEach { + + InModuleScope -ScriptBlock { + + #Mock -CommandName Test-EnvironmentVariable -MockWith { return 'SomePath' } + Mock -CommandName Invoke-UserProfileRegRemoval { + + } + + Mock -CommandName 'PromptForConfirmation' -MockWith { + param ($ComputerName, $ItemCount, $AuditOnly, $Confirm) + return $Confirm + } + + Mock -CommandName Get-SIDFromUsername -MockWith { + param($Username, $ComputerName) + + # Simulating behavior: Return SIDs for known users, $null for unknown users + switch ($Username) + { + 'testuser1' + { + return 'S-1-5-21-1234567890-123456789-123456789-1001' + } + 'testuser2' + { + return 'S-1-5-21-1234567890-123456789-123456789-1002' + } + default + { + return $null + } # Simulate unresolved users + } + } + + Mock -CommandName Resolve-UsernamesToSIDs -MockWith { + param($Usernames, $ComputerName) + + $SIDs = $Usernames | ForEach-Object { + Get-SIDFromUsername -Username $_ -ComputerName $ComputerName + } + + return $SIDs + } + + #Mock -CommandName 'PromptForConfirmation' -MockWith { return $true } + + Mock -CommandName "ShouldContinueWrapper" -MockWith { } + + Mock Invoke-UserProfileAudit { + param($IgnoreSpecial, $computerName) + + $objects = @() + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1003" -ProfilePath "$env:SystemDrive\Users\TestUserSpecial" -IsOrphaned $false -ComputerName $computerName -IsSpecial $true + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1001" -ProfilePath "$env:SystemDrive\Users\TestUser1" -IsOrphaned $false -ComputerName $computerName -IsSpecial $false + $objects += New-UserProfileObject -SID "S-1-5-21-1234567890-1002" -ProfilePath "$env:SystemDrive\Users\TestUser2" -IsOrphaned $false -ComputerName $computerName -IsSpecial $false + if ($IgnoreSpecial) + { + return $objects | Where-Object { $_.IsSpecial -eq $false } + } + else + { + return $objects + } + } + + } -ModuleName $script:dscModuleName + } + + + ### Input Tests ### + Context 'Using SIDSet' { + It 'Should call Invoke-UserProfileRegRemoval when using SIDSet' { + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + $computerName = 'TestComputer' + + # Run the function + $Return = Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName $computerName -Force + + # Assert that Invoke-UserProfileRegRemoval is called with the correct parameters + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SIDs -contains 'S-1-5-21-1234567890-123456789-123456789-1001' + } + } + + It 'Should call Invoke-UserProfileRegRemoval in audit mode when using SIDSet' { + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + $computerName = 'TestComputer' + + # Run the function + $Return = Remove-UserProfilesFromRegistry -SIDs $SIDs -AuditOnly -ComputerName $computerName -Force + + # Assert that Invoke-UserProfileRegRemoval is called in audit mode + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $AuditOnly -eq $true -and $SIDs -contains 'S-1-5-21-1234567890-123456789-123456789-1001' + } + } + + It 'Should process multiple SIDs correctly' { + InModuleScope -ScriptBlock { + + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001', 'S-1-5-21-1234567890-123456789-123456789-1002') + $computerName = 'TestComputer' + + # Run the function + Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName $computerName -Force + + # Assert that Invoke-UserProfileRegRemoval is called for both SIDs + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It -ParameterFilter { + $SIDs -contains 'S-1-5-21-1234567890-123456789-123456789-1001' -or $SIDs -contains 'S-1-5-21-1234567890-123456789-123456789-1002' -and $ComputerName -eq 'TestComputer' + } + + } + } + + It 'Should prompt for confirmation with Confirm:$true even if Force:$true' { + InModuleScope -ScriptBlock { + + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + $computerName = 'TestComputer' + + # Mock ShouldContinueWrapper to simulate confirmation prompt + Mock -CommandName 'ShouldContinueWrapper' -MockWith { return $true } + + # Mock the confirmation prompt for both computers + Mock -CommandName 'PromptForConfirmation' -MockWith { + return ShouldContinueWrapper -Context $PSCmdlet -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + # Run the function with Confirm enabled and Force true + Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName $computerName -Force:$true -Confirm:$true + + # Assert that PromptForConfirmation was called + Assert-MockCalled -CommandName 'ShouldContinueWrapper' -Exactly -Times 1 -Scope It + + # Assert that Invoke-UserProfileRegRemoval was called after confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It -ParameterFilter { + $Confirm -eq $true -and $Force -eq $true -and $ComputerName -eq 'TestComputer' + } + } + } + + It 'Should bypass confirmation with Confirm:$false and Force:$true' { + InModuleScope -ScriptBlock { + + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + $computerName = 'TestComputer' + + Mock -CommandName 'PromptForConfirmation' -MockWith { + return $true + } + + # Run the function with Confirm disabled and Force true + Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName $computerName -Force:$true -Confirm:$false + + + # Assert that PromptForConfirmation was not called + Assert-MockCalled -CommandName 'ShouldContinueWrapper' -Exactly -Times 0 -Scope It + + # Assert that Invoke-UserProfileRegRemoval was called after bypassing confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It -ParameterFilter { + $Confirm -eq $false -and $Force -eq $true -and $ComputerName -eq 'TestComputer' + } + } + } + + It 'Should default to local computer if ComputerName is not provided' { + InModuleScope -ScriptBlock { + + $SIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + + # Run the function without specifying ComputerName + Remove-UserProfilesFromRegistry -SIDs $SIDs -Force + + # Assert that Invoke-UserProfileRegRemoval is called with local computer name + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq $env:COMPUTERNAME + } + } + } + + + } + + Context 'Using UserNameSet' { + It 'Should resolve usernames to SIDs and call Invoke-UserProfileRegRemoval' { + $Usernames = @('testuser1', 'testuser2') + $computerName = 'TestComputer' + $ExpectedSIDs = @('S-1-5-21-1234567890-123456789-123456789-1001', 'S-1-5-21-1234567890-123456789-123456789-1002') + + # Run the function + $return = Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force + + # Assert that Resolve-UsernamesToSIDs was called + should -Invoke -CommandName Resolve-UsernamesToSIDs -Exactly -Times 1 -Scope It -ParameterFilter { + $Usernames -contains 'testuser1' -and $Usernames -contains 'testuser2' + } + + # Assert that Invoke-UserProfileRegRemoval is called with resolved SIDs + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq 'S-1-5-21-1234567890-123456789-123456789-1001' + } + + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq 'S-1-5-21-1234567890-123456789-123456789-1002' + } + + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It + + } + + It 'Should resolve usernames to SIDs and audit profiles without deletion' { + $Usernames = @('testuser1', 'testuser2') + $computerName = 'TestComputer' + $ExpectedSIDs = @('S-1-5-21-1234567890-123456789-123456789-1001', 'S-1-5-21-1234567890-123456789-123456789-1002') + + # Run the function + $return = Remove-UserProfilesFromRegistry -Usernames $Usernames -AuditOnly -ComputerName $computerName -Force + + # Assert that Resolve-UsernamesToSIDs was called + should -Invoke -CommandName Resolve-UsernamesToSIDs -Exactly -Times 1 -Scope It -ParameterFilter { + $Usernames -contains 'testuser1' -and $Usernames -contains 'testuser2' + } + + # Assert that Invoke-UserProfileRegRemoval was called in audit mode + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $AuditOnly -eq $true -and $SID -eq 'S-1-5-21-1234567890-123456789-123456789-1001' + } + } + + It 'Should Call Resolve-UserNamesToSIDs when Usernames are provided' { + $Usernames = @('testuser1', 'testuser2') + $computerName = 'TestComputer' + $ExpectedSIDs = @('S-1-5-21-1234567890-123456789-123456789-1001', 'S-1-5-21-1234567890-123456789-123456789-1002') + + # Run the function + $return = Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force + + # Assert that Resolve-UsernamesToSIDs was called + should -Invoke -CommandName Resolve-UsernamesToSIDs -Exactly -Times 1 -Scope It -ParameterFilter { + $Usernames -contains 'testuser1' -and $Usernames -contains 'testuser2' + } + } + + + It 'Should handle partial resolution of usernames' { + $Usernames = @('testuser1', 'unknownuser') + $computerName = 'TestComputer' + $ExpectedSIDs = @('S-1-5-21-1234567890-123456789-123456789-1001') + + # Run the function + $return = Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force + + # Assert that Resolve-UsernamesToSIDs was called + should -Invoke -CommandName Resolve-UsernamesToSIDs -Exactly -Times 1 + + # Assert that Invoke-UserProfileRegRemoval is called with resolved SIDs only + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq 'S-1-5-21-1234567890-123456789-123456789-1001' + } + + + should -Invoke -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 0 -Scope It -ParameterFilter { + $SID -eq $null + } + + } + + It 'Should handle invalid usernames gracefully' { + $Usernames = @('invaliduser1', 'invaliduser2') + $computerName = 'TestComputer' + + # Mock Resolve-UsernamesToSIDs to return $null for invalid users + Mock -CommandName Resolve-UsernamesToSIDs -MockWith { return @() } + + # Run the function + $return = Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force -ErrorAction SilentlyContinue + + # Assert that Resolve-UsernamesToSIDs was called and returned $null + Assert-MockCalled -CommandName Resolve-UsernamesToSIDs -Exactly -Times 1 -Scope It + + # Assert that Invoke-UserProfileRegRemoval was not called since no SIDs were resolved + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 0 -Scope It + } + + It 'Should throw an error if an empty username array is provided' { + $Usernames = @() + $computerName = 'TestComputer' + + # Run the function with an empty username array and ensure it throws + { Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force } | Should -Throw + } + + It 'Should prompt for confirmation with Confirm:$true even if Force:$true' { + InModuleScope -ScriptBlock { + $Usernames = @('testuser1') + $computerName = 'TestComputer' + # Mock ShouldContinueWrapper to simulate confirmation prompt + Mock -CommandName 'ShouldContinueWrapper' -MockWith { return $true } + + # Mock the confirmation prompt + Mock -CommandName 'PromptForConfirmation' -MockWith { + return ShouldContinueWrapper -Context $PSCmdlet -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + + # Run the function with Confirm enabled and Force true + Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force:$true -Confirm:$true + + # Assert that PromptForConfirmation was called + Assert-MockCalled -CommandName 'PromptForConfirmation' -Exactly -Times 1 -Scope It + + # Assert that Invoke-UserProfileRegRemoval was called after confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It + + } + } + + It 'Should bypass confirmation with Confirm:$false and Force:$true' { + + InModuleScope -ScriptBlock { + + $Usernames = @('testuser1') + $computerName = 'TestComputer' + + # Mock the confirmation prompt to ensure it's not called + Mock -CommandName 'PromptForConfirmation' -MockWith { + param($ComputerName, $ItemCount, $AuditOnly, $Confirm, $context) + if ($confirm) + { + return ShouldContinueWrapper -Context $context -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + else + { + return $true + } + } + + # Run the function with Confirm disabled and Force true + Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force:$true -Confirm:$false + + # Assert that PromptForConfirmation was not called + Assert-MockCalled -CommandName 'PromptForConfirmation' -Exactly -Times 1 -Scope It + + # Assert that PromptForConfirmation was called + Assert-MockCalled -CommandName 'ShouldContinueWrapper' -Exactly -Times 0 -Scope It + + # Assert that Invoke-UserProfileRegRemoval was called after bypassing confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It + + } + } + + + It 'Should default to local computer if ComputerName is not provided' { + $Usernames = @('testuser1') + + # Run the function without specifying ComputerName + Remove-UserProfilesFromRegistry -Usernames $Usernames -Force + + # Assert that Invoke-UserProfileRegRemoval is called with local computer name + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq $env:COMPUTERNAME + } + } + + + + } + + Context 'Using UserProfileSet' { + + It 'Should prompt for confirmation before deleting profiles' { + InModuleScope -ScriptBlock { + + # Create mock user profile objects + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Mock ShouldContinueWrapper to simulate user saying "yes" + Mock -CommandName 'ShouldContinueWrapper' -MockWith { return $true } + + # Mock the confirmation prompt (simulating that confirmation happens) + Mock -CommandName 'PromptForConfirmation' -MockWith { + # Ensure ShouldContinueWrapper is triggered when Confirm is true + return ShouldContinueWrapper -Context $PSCmdlet -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + + # Run the function with Confirm enabled + Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Confirm:$true + + # Assert that PromptForConfirmation was called once + Assert-MockCalled -CommandName 'PromptForConfirmation' -Exactly -Times 1 -Scope It + + # Assert that ShouldContinueWrapper was called once as part of confirmation + Assert-MockCalled -CommandName 'ShouldContinueWrapper' -Exactly -Times 1 -Scope It + + # Assert that Invoke-UserProfileRegRemoval is called after confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -ParameterFilter { + $Confirm -eq $true -and $Force -eq $false + } + + } + } + + + It 'Should bypass confirmation and delete profiles with -Force -Confirm:$false' { + InModuleScope -ScriptBlock { + + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + Mock -CommandName 'PromptForConfirmation' -MockWith { + param($ComputerName, $ItemCount, $AuditOnly, $Confirm, $context) + if ($confirm) + { + return ShouldContinueWrapper -Context $context -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + else + { + return $true + } + } + + # Run the function with Confirm enabled + Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force:$true -Confirm:$false + + # Assert that PromptForConfirmation was called + Assert-MockCalled -CommandName 'ShouldContinueWrapper' -Exactly -Times 0 -Scope It + + # Assert that Invoke-UserProfileRegRemoval is called after confirmation + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 + + } + } + + + It 'Should call Invoke-UserProfileRegRemoval with UserProfiles' { + InModuleScope -ScriptBlock { + # Create mock UserProfile objects using New-UserProfileObject + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + $GroupedProfiles = $MockUserProfileObjects | Group-Object -Property ComputerName + + $Profile1 = ($GroupedProfiles.Group)[0] + $Profile2 = ($GroupedProfiles.Group)[1] + + + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It + + # Assert that Invoke-UserProfileRegRemoval is called with the UserProfiles + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq $Profile1.SID -and $computerName -eq $Profile1.computerName + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq $Profile2.SID -and $computerName -eq $Profile2.computerName + } + + } + } + + It 'Should call Invoke-UserProfileRegRemoval in audit mode with UserProfiles' { + + InModuleScope -ScriptBlock { + # Create mock UserProfile objects using New-UserProfileObject + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Run the function + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -AuditOnly + + $GroupedProfiles = $MockUserProfileObjects | Group-Object -Property ComputerName + + $Profile1 = ($GroupedProfiles.Group)[0] + $Profile2 = ($GroupedProfiles.Group)[1] + + + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It -ParameterFilter { + $AuditOnly -eq $true + } + + # Assert that Invoke-UserProfileRegRemoval is called with the UserProfiles + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq $Profile1.SID -and $computerName -eq $Profile1.computerName -and $AuditOnly -eq $true + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $SID -eq $Profile2.SID -and $computerName -eq $Profile2.computerName + } + + } + } + + It 'Should group UserProfiles by ComputerName and call processing per group' { + InModuleScope -ScriptBlock { + # Create mock UserProfile objects using New-UserProfileObject + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer2' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + # Assert that Invoke-UserProfileRegRemoval is called for each computer + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer1' + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer2' + } + + + } + } + + It 'Should process profiles grouped by ComputerName' { + InModuleScope -ScriptBlock { + # Create mock UserProfile objects for different computers + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer2' -IsSpecial:$false + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + # Assert that Invoke-UserProfileRegRemoval is called for each computer + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer1' + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer2' + } + } + } + + It 'Should prompt and process profiles for each unique computer' { + InModuleScope -ScriptBlock { + + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer2' -IsSpecial:$false + + # Mock ShouldContinueWrapper to simulate the prompt and user saying "yes" + Mock -CommandName 'ShouldContinueWrapper' -MockWith { return $true } + + # Mock the confirmation prompt for both computers + Mock -CommandName 'PromptForConfirmation' -MockWith { + return ShouldContinueWrapper -Context $PSCmdlet -QueryMessage "Are you sure?" -CaptionMessage "Confirm Deletion" + } + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Confirm:$true + + # Assert that confirmation prompt was shown for both computers + Assert-MockCalled -CommandName 'PromptForConfirmation' -Exactly -Times 2 -Scope It + + # Assert that profiles were processed for both computers + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 2 -ParameterFilter { + $Computername -eq 'TestComputer1' -or $Computername -eq 'TestComputer2' + } + } + } + + It 'Should process single profile for a computer correctly' { + InModuleScope -ScriptBlock { + # Create mock UserProfile object for one computer + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + # Assert that Invoke-UserProfileRegRemoval is called once for the computer + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer1' + } + } + } + } + + + ### Mode Tests ### + Context 'Audit Mode' { + It 'Should return profiles for audit and not remove them' { + InModuleScope -ScriptBlock { + # Create mock UserProfile objects for auditing + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + + + Mock -CommandName Invoke-UserProfileRegRemoval { + param($SID, $ComputerName, $AuditOnly, $Force) + + # Prepare the deletion result parameters + $deletionResultParams = @{ + SID = $SID + ProfilePath = "FakeProfilePath" + ComputerName = $ComputerName + DeletionSuccess = $false + DeletionMessage = "Profile not removed." + } + + # If in audit mode, output an audit-only result directly to the pipeline and return + if ($AuditOnly) + { + $deletionResultParams.DeletionSuccess = $true + $deletionResultParams.DeletionMessage = "Audit only, no deletion performed." + New-ProfileDeletionResult @deletionResultParams + } + + } + + # Run the function in -AuditOnly mode + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -AuditOnly + + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It -ParameterFilter { + $AuditOnly -eq $true -and $force -eq $false + } + + # Assert that profiles were audited but not removed + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $AuditOnly -eq $true -and $SID -eq 'S-1-5-21-1234567890-1001' + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $AuditOnly -eq $true -and $SID -eq 'S-1-5-21-1234567890-1002' + } + + # Assert that no profiles were actually removed (simulate no action in Audit mode) + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 0 -Scope It -ParameterFilter { + $Force -eq $true + } + + # Assert that the function returned the correct profiles for auditing + foreach ($item in $return) + { + $item.GetType().Name | Should -Be 'ProfileDeletionResult' + } + $return.Count | Should -Be $MockUserProfileObjects.Count + } + } + + It 'Should throw an error when empty lists are provided' { + InModuleScope -ScriptBlock { + $EmptySIDs = @() + $EmptyUsernames = @() + $EmptyUserProfiles = @() + + # Run the function with empty SIDs + { Remove-UserProfilesFromRegistry -SIDs $EmptySIDs -AuditOnly -Force } | Should -Throw + + # Run the function with empty Usernames + { Remove-UserProfilesFromRegistry -Usernames $EmptyUsernames -AuditOnly -Force } | Should -Throw + + # Run the function with empty UserProfiles + { Remove-UserProfilesFromRegistry -UserProfiles $EmptyUserProfiles -AuditOnly -Force } | Should -Throw + } + } + + It 'Should bypass deletion and only audit when AuditOnly is set' { + InModuleScope -ScriptBlock { + + # Create mock user profile objects + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Mock Invoke-UserProfileRegRemoval to simulate audit mode + Mock -CommandName 'Invoke-UserProfileRegRemoval' -MockWith { + param($SID, $ComputerName, $AuditOnly, $Force) + New-ProfileDeletionResult -SID $SID -ProfilePath "FakePath" -DeletionSuccess $false -DeletionMessage "Audit mode" -ComputerName $ComputerName + } + + # Run the function in audit mode + $Return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -AuditOnly + + # Assert that no deletion was attempted + Assert-MockCalled -CommandName 'Invoke-UserProfileRegRemoval' -Exactly -Times 1 -ParameterFilter { + $AuditOnly -eq $true + } + + # Ensure the returned result contains audit information + $Return[0].DeletionMessage | Should -Be 'Audit mode' + } + } + } + + ### Error Handling ### + Context "Error Tests" { + + It 'Should throw error if no SIDs, Usernames, or UserProfiles are provided' { + $computerName = "TestComputer" + { Remove-UserProfilesFromRegistry -ComputerName $computerName -Force } | Should -Throw + } + + It 'Should throw empty SIDs array' { + + $computerName = "TestComputer" + $SIDs = @() + { Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName $computerName -Force } | Should -Throw + } + + It 'Should throw empty Usernames array' { + $computerName = "TestComputer" + $Usernames = @() + { Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName $computerName -Force } | Should -Throw + } + + It 'Should throw empty UserProfiles array' { + $computerName = "TestComputer" + $UserProfiles = @() + { Remove-UserProfilesFromRegistry -UserProfiles $UserProfiles -ComputerName $computerName -Force } | Should -Throw + } + + It 'Should handle exceptions thrown during profile removal' { + InModuleScope -ScriptBlock { + # Mock UserProfile objects + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser' -IsOrphaned $false -ComputerName 'TestComputer' -IsSpecial:$false + + # Mock Invoke-UserProfileRegRemoval to throw an error + Mock -CommandName Invoke-UserProfileRegRemoval -MockWith { throw "Test exception" } + + # Run the function and catch the error + { Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force } | Should -Throw "Test exception" + + # Optionally, check if the function handled the error gracefully + # Assert that an appropriate error message is logged or returned + } + } + + It 'Should throw an error if no UserProfiles, SIDs, or Usernames are provided' { + $computerName = "TestComputer" + { Remove-UserProfilesFromRegistry -ComputerName $computerName -Force } | Should -Throw + } + + + } + + ### Large Input Sets ### + Context 'Large Input Sets - Maximum Profiles' { + It 'Should process a large number of profiles without errors' { + InModuleScope -ScriptBlock { + # Mock a large number of UserProfile objects + $MockUserProfileObjects = 1..100 | ForEach-Object { + New-UserProfileObject -SID "S-1-5-21-1234567890-$_" -ProfilePath "C:\Users\testuser$_" -IsOrphaned $false -ComputerName 'TestComputer' -IsSpecial:$false + } + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + # Assert that all profiles were processed without errors + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 100 + } + } + } + +} + + + +<# + It 'Should call Invoke-UserProfileRegRemoval in audit mode with UserProfiles' { + + InModuleScope -ScriptBlock { + # Create mock UserProfile objects using New-UserProfileObject + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force -AuditOnly + + $GroupedProfiles = $MockUserProfileObjects | Group-Object -Property ComputerName + + $ProfilesGroup = ($GroupedProfiles.Group)[0] + + + # Assert that Invoke-UserProfileRegRemoval is called with the UserProfiles + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $Profiles -contains $MockUserProfileObjects[0] ` + -and $Profiles -contains $MockUserProfileObjects[1] ` + -and $computerName -eq "TestComputer1" -and $AuditOnly -eq $true + } + + } + } + + It 'Should group UserProfiles by ComputerName and call processing per group' { + InModuleScope -ScriptBlock { + # Create mock UserProfile objects using New-UserProfileObject + $MockUserProfileObjects = @() + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser1' -IsOrphaned $false -ComputerName 'TestComputer2' -IsSpecial:$false + $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1002' -ProfilePath 'C:\Users\testuser2' -IsOrphaned $false -ComputerName 'TestComputer1' -IsSpecial:$false + + + # Run the function + $return = Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force + + # Assert that Invoke-UserProfileRegRemoval is called for each computer + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 2 -Scope It + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer1' + } + + Assert-MockCalled -CommandName Invoke-UserProfileRegRemoval -Exactly -Times 1 -Scope It -ParameterFilter { + $ComputerName -eq 'TestComputer2' + } + + + } + } + + + } + + Context "Error Tests" { + + It 'Should throw error if no SIDs, Usernames, or UserProfiles are provided' { + { Remove-UserProfilesFromRegistry -ComputerName 'TestComputer' -Force } | Should -Throw + } + + + } + + Context "Empty Input Tests" { + + It 'Should throw empty SIDs array' { + $SIDs = @() + { Remove-UserProfilesFromRegistry -SIDs $SIDs -ComputerName 'TestComputer' -Force } | Should -Throw + } + + It 'Should throw empty Usernames array gracefully' { + $Usernames = @() + { Remove-UserProfilesFromRegistry -Usernames $Usernames -ComputerName 'TestComputer' -Force } | Should -Throw + } + + It 'Should throw empty UserProfiles array gracefully' { + $UserProfiles = @() + { Remove-UserProfilesFromRegistry -UserProfiles $UserProfiles -ComputerName 'TestComputer' -Force } | Should -Throw + } + + + } + +} + + + + BeforeEach { InModuleScope -scriptblock { @@ -32,7 +901,7 @@ Describe 'Remove-UserProfilesFromRegistry' -Tag 'Public' { # Mock necessary functions Mock Get-DirectoryPath { "C:\LHStuff\RegBackUp" } Mock Test-DirectoryExistence { $true } - Mock Open-RegistryKey { New-MockObject -Type Microsoft.Win32.RegistryKey } + Mock Open-RegistryKey { New-MockObject -Type Microsoft.Win32.RegistryKey -Methods @{ Dispose = {} } } Mock Invoke-UserProfileAudit { param($IgnoreSpecial, $computerName) @@ -50,39 +919,68 @@ Describe 'Remove-UserProfilesFromRegistry' -Tag 'Public' { } } - Mock Invoke-ProcessProfileRemoval { - param($SID, $computerName) - New-ProfileDeletionResult -SID $SID -ProfilePath "$env:SystemDrive\Users\TestUser" -DeletionSuccess $true -DeletionMessage "Profile removed successfully." -ComputerName $computerName - } - - Mock Invoke-UserProfileProcessing { + Mock Invoke-UserProfileRegRemoval { param($ComputerName, $SIDs, $Profiles, $RegistryPath, $ProfileFolderPath, $RegistryHive, $Force, $AuditOnly, $Confirm) - # Simulate successful removal of profiles - foreach ($sid in $SIDs) + + # Initialize $deletionResults as an empty array + $deletionResults = @() + + if ($SIDs) { - if ($AuditOnly) + foreach ($SID in $SIDs) + { + Invoke-SingleProfileAction -SID $SID -AuditResults $null -ComputerName $ComputerName -BaseKey $null -Force:$Force -AuditOnly:$AuditOnly -DeletionResults ([ref]$deletionResults) -Confirm:$Confirm + } + } + if ($Profiles) + { + foreach ($Profile in $Profiles) { + Invoke-SingleProfileAction -SID $Profile.SID -AuditResults $null -SelectedProfile $Profile -ComputerName $ComputerName -BaseKey $null -Force:$Force -AuditOnly:$AuditOnly -DeletionResults ([ref]$deletionResults) -Confirm:$Confirm + } + } - return New-ProfileDeletionResult -SID $sid -ProfilePath "$env:SystemDrive\Users\TestUser" -DeletionSuccess $true -DeletionMessage "Audit only, no deletion performed." -ComputerName $ComputerName + return $deletionResults # Return the accumulated deletion results + } - } + Mock Invoke-SingleProfileAction { + param($SID, $AuditResults, $SelectedProfile, $BaseKey, [ref]$DeletionResults, $Force, $AuditOnly, $Confirm) + + # Call Invoke-ProcessProfileRemoval to simulate the removal or auditing of the profile + $results = Invoke-ProcessProfileRemoval -SID $SID -SelectedProfile $SelectedProfile -BaseKey $BaseKey -AuditOnly:$AuditOnly -ComputerName $ComputerName -Confirm:$Confirm + + # Append the result to the DeletionResults array + $DeletionResults.Value += $results + } - New-ProfileDeletionResult -SID $sid -ProfilePath "$env:SystemDrive\Users\TestUser" -DeletionSuccess $true -DeletionMessage "Profile removed successfully." -ComputerName $ComputerName + Mock Invoke-ProcessProfileRemoval { + param($SID, $SelectedProfile, $BaseKey, $AuditOnly, $ComputerName, $Confirm) + + # Simulate the result based on whether AuditOnly is true or not + if ($AuditOnly) + { + return New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $true -DeletionMessage "Audit only, no deletion performed." -ComputerName $ComputerName + } + else + { + return New-ProfileDeletionResult -SID $SID -ProfilePath $SelectedProfile.ProfilePath -DeletionSuccess $true -DeletionMessage "Profile removed successfully." -ComputerName $ComputerName } } + } -ModuleName $Script:dscModuleName } Context 'When profiles are successfully removed' { It 'Should remove the user profile successfully' { - $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $env:COMPUTERNAME -Confirm:$false + + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $env:COMPUTERNAME -Confirm:$false -force $result | Should -HaveCount 1 $result[0].DeletionSuccess | Should -Be $true $result[0].DeletionMessage | Should -Be "Profile removed successfully." } It 'Should remove multiple user profiles successfully' { - $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001", "S-1-5-21-1234567890-1002") -ComputerName $env:COMPUTERNAME -Confirm:$false + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001", "S-1-5-21-1234567890-1002") -ComputerName $env:COMPUTERNAME -Confirm:$false -force $result | Should -HaveCount 2 $result[0].DeletionSuccess | Should -Be $true $result[0].DeletionMessage | Should -Be "Profile removed successfully." @@ -91,7 +989,7 @@ Describe 'Remove-UserProfilesFromRegistry' -Tag 'Public' { } It 'Should only audit the profile without removing it when AuditOnly is set' { - $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -AuditOnly -ComputerName $env:COMPUTERNAME -Confirm:$false + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -AuditOnly -ComputerName $env:COMPUTERNAME -Confirm:$false -force $result | Should -HaveCount 1 $result[0].DeletionSuccess | Should -Be $true $result[0].DeletionMessage | Should -Be "Audit only, no deletion performed." @@ -99,24 +997,17 @@ Describe 'Remove-UserProfilesFromRegistry' -Tag 'Public' { } Context 'When confirmation is required' { - It 'Should prompt for confirmation before removing profile' { - $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -WhatIf - $result | Should -BeNullOrEmpty - Should -Invoke Invoke-ProcessProfileRemoval -Exactly 0 -Scope It - } - It 'Should remove profile when confirmation is bypassed' { + # Using -Confirm:$false to bypass confirmation - $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -Confirm:$false + $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -Confirm:$false -Force $result | Should -HaveCount 1 $result[0].DeletionSuccess | Should -Be $true Should -Invoke Invoke-ProcessProfileRemoval -Exactly 1 -Scope It } } -} -<# Context 'When profiles are successfully removed' { It 'Should remove the user profile successfully' { $result = Remove-UserProfilesFromRegistry -SIDs @("S-1-5-21-1234567890-1001") -ComputerName $env:COMPUTERNAME -Confirm:$false From 7f15837533951bbdb90fb5b15faea96171268041 Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Mon, 23 Sep 2024 23:49:21 -0700 Subject: [PATCH 21/23] updated enviroment Variables --- .../Remove-OrphanedProfiles.ps1 | 5 +- source/Private/Get-SIDProfileInfo.ps1 | 4 +- source/Private/Get-UserFolders.ps1 | 2 +- source/Private/Remove-RegistryKeyForSID.ps1 | 54 ---- source/Private/Remove-SIDProfile.ps1 | 102 -------- .../Invoke-UserProfileRegRemoval.ps1 | 34 +-- source/Public/Get-OrphanedProfiles.ps1 | 2 +- source/Public/Get-UserProfilesFromFolders.ps1 | 2 +- source/Public/Invoke-UserProfileAudit.ps1 | 2 +- .../Remove-UserProfilesFromRegistry.ps1 | 34 ++- source/prefix.ps1 | 12 +- tests/Helpers/MockedProfListReg.ps1 | 8 +- tests/Intergration/PublicFunctions.tests.ps1 | 242 +++++++++++++++++- .../Remove-RegistryKeyForSID.tests.ps1 | 111 -------- .../Unit/Private/Remove-SIDProfile.tests.ps1 | 146 ----------- .../Remove-UserProfilesFromRegistry.tests.ps1 | 15 +- 16 files changed, 298 insertions(+), 477 deletions(-) delete mode 100644 source/Private/Remove-RegistryKeyForSID.ps1 delete mode 100644 source/Private/Remove-SIDProfile.ps1 delete mode 100644 tests/Unit/Private/Remove-RegistryKeyForSID.tests.ps1 delete mode 100644 tests/Unit/Private/Remove-SIDProfile.tests.ps1 diff --git a/source/NotImplemented/Remove-OrphanedProfiles.ps1 b/source/NotImplemented/Remove-OrphanedProfiles.ps1 index 770a93f..048702c 100644 --- a/source/NotImplemented/Remove-OrphanedProfiles.ps1 +++ b/source/NotImplemented/Remove-OrphanedProfiles.ps1 @@ -28,7 +28,7 @@ function Remove-OrphanedProfiles [string]$ComputerName, [Parameter(Mandatory = $false)] - [string]$ProfileFolderPath = "$env:SystemDrive\Users", + [string]$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath, [switch]$IgnoreSpecial ) @@ -57,6 +57,3 @@ function Remove-OrphanedProfiles # Step 4: Return the results of the removal process return $removalResults } - - - diff --git a/source/Private/Get-SIDProfileInfo.ps1 b/source/Private/Get-SIDProfileInfo.ps1 index a20a56e..926d751 100644 --- a/source/Private/Get-SIDProfileInfo.ps1 +++ b/source/Private/Get-SIDProfileInfo.ps1 @@ -43,10 +43,10 @@ function Get-SIDProfileInfo [CmdletBinding()] param ( [string]$ComputerName = $env:COMPUTERNAME, - [string]$RegistryPath = $env:GetSIDProfileInfo_RegistryPath + [string]$RegistryPath = $env:WinProfileOps_RegistryPath ) - $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName -Writable $false -RegistryHive $env:GetSIDProfile_RegistryHive + $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName -Writable $false -RegistryHive $env:WinProfileOps_RegistryHive # Handle null or empty registry key if (-not $ProfileListKey) diff --git a/source/Private/Get-UserFolders.ps1 b/source/Private/Get-UserFolders.ps1 index 891dd9b..a8ffc24 100644 --- a/source/Private/Get-UserFolders.ps1 +++ b/source/Private/Get-UserFolders.ps1 @@ -47,7 +47,7 @@ function Get-UserFolders [CmdletBinding()] param ( [string]$ComputerName = $env:COMPUTERNAME, - [string]$ProfileFolderPath = "$env:SystemDrive\Users" + [string]$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath ) $IsLocal = ($ComputerName -eq $env:COMPUTERNAME) diff --git a/source/Private/Remove-RegistryKeyForSID.ps1 b/source/Private/Remove-RegistryKeyForSID.ps1 deleted file mode 100644 index 42e5ee2..0000000 --- a/source/Private/Remove-RegistryKeyForSID.ps1 +++ /dev/null @@ -1,54 +0,0 @@ -<# -.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 - { - # 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 -Confirm:$false - } - else - { - Write-Verbose "Removal of registry key for SID '$SID' was skipped." - return $false - } - } - 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 deleted file mode 100644 index a8caf96..0000000 --- a/source/Private/Remove-SIDProfile.ps1 +++ /dev/null @@ -1,102 +0,0 @@ -<# -.SYNOPSIS - Coordinates the deletion of a profile registry key for a given SID. - -.DESCRIPTION - 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) of the profile to be deleted. - -.PARAMETER ProfileListKey - 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 profile registry key resides. Defaults to the current computer. - -.PARAMETER ProfilePath - 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 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 -{ - [outputtype([ProfileDeletionResult])] - # 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 - ) - - try - { - # 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 - ) - } - } - 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'. Error: $_", - $ComputerName - ) - } -} diff --git a/source/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.ps1 b/source/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.ps1 index ce45d90..3b303ab 100644 --- a/source/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.ps1 +++ b/source/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.ps1 @@ -78,28 +78,32 @@ function Invoke-UserProfileRegRemoval # Initialize a flag to determine if processing should continue $continueProcessing = $true - # Perform audit once for the computer - $BaseKey = Open-RegistryKey -ComputerName $ComputerName -RegistryHive $RegistryHive -RegistryPath $RegistryPath - - # Exit early if the registry key cannot be opened - if (-not $BaseKey -or $null -eq $BaseKey) + try { - Write-Error "Failed to open registry key on computer $ComputerName" - $continueProcessing = $false # Set the flag to prevent processing - return # Stop the function entirely if BaseKey is null - } + # Try to open the registry key + $BaseKey = Open-RegistryKey -ComputerName $ComputerName -RegistryHive $RegistryHive -RegistryPath $RegistryPath -ErrorAction SilentlyContinue - # Perform the audit once and store the results if BaseKey is valid - if ($continueProcessing) - { + # Check if the registry key is valid + if (-not $BaseKey) + { + throw "Failed to open registry key on computer $ComputerName" + } + + # Perform the audit if the BaseKey is valid $userProfileAudit = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial if (-not $userProfileAudit) { - Write-Error "Failed to audit user profiles on computer $ComputerName" - $continueProcessing = $false # Set the flag to prevent processing - return # Stop the function entirely if the audit fails + throw "Failed to audit user profiles on computer $ComputerName" } + + } + catch + { + # Catch any exceptions that occur during the process + Write-Error $_.Exception.Message + $continueProcessing = $false # Set the flag to prevent further processing + return # Exit the function early if an error occurs } } diff --git a/source/Public/Get-OrphanedProfiles.ps1 b/source/Public/Get-OrphanedProfiles.ps1 index 72dcd1e..f5b5656 100644 --- a/source/Public/Get-OrphanedProfiles.ps1 +++ b/source/Public/Get-OrphanedProfiles.ps1 @@ -33,7 +33,7 @@ function Get-OrphanedProfiles [OutputType([UserProfile[]])] param ( [string]$ComputerName = $env:COMPUTERNAME, - [string]$ProfileFolderPath = "$env:SystemDrive\Users", + [string]$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath, [switch]$IgnoreSpecial ) diff --git a/source/Public/Get-UserProfilesFromFolders.ps1 b/source/Public/Get-UserProfilesFromFolders.ps1 index 844f566..4f6793f 100644 --- a/source/Public/Get-UserProfilesFromFolders.ps1 +++ b/source/Public/Get-UserProfilesFromFolders.ps1 @@ -35,7 +35,7 @@ function Get-UserProfilesFromFolders [CmdletBinding()] param ( [string]$ComputerName = $env:COMPUTERNAME, - [string]$ProfileFolderPath = "$env:SystemDrive\Users" + [string]$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath ) try diff --git a/source/Public/Invoke-UserProfileAudit.ps1 b/source/Public/Invoke-UserProfileAudit.ps1 index fc221b1..8cf8a6d 100644 --- a/source/Public/Invoke-UserProfileAudit.ps1 +++ b/source/Public/Invoke-UserProfileAudit.ps1 @@ -53,7 +53,7 @@ function Invoke-UserProfileAudit [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [string]$ComputerName = $env:COMPUTERNAME, - [string]$ProfileFolderPath = "$env:SystemDrive\Users", + [string]$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath, [switch]$IgnoreSpecial ) diff --git a/source/Public/Remove-UserProfilesFromRegistry.ps1 b/source/Public/Remove-UserProfilesFromRegistry.ps1 index 19b7b42..7a031f7 100644 --- a/source/Public/Remove-UserProfilesFromRegistry.ps1 +++ b/source/Public/Remove-UserProfilesFromRegistry.ps1 @@ -70,6 +70,7 @@ function Remove-UserProfilesFromRegistry [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "SIDSet")] [string[]]$SIDs, + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "UserNameSet")] [string[]]$Usernames, @@ -82,9 +83,9 @@ function Remove-UserProfilesFromRegistry Begin { # Retrieve necessary environment variables - $RegistryPath = Test-EnvironmentVariable -Name 'GetSIDProfileInfo_RegistryPath' - $ProfileFolderPath = Test-EnvironmentVariable -Name 'GetSIDProfileInfo_ProfileFolderPath' - $RegistryHive = Test-EnvironmentVariable -Name 'GetSIDProfile_RegistryHive' + $RegistryPath = Test-EnvironmentVariable -Name 'WinProfileOps_RegistryPath' + $ProfileFolderPath = Test-EnvironmentVariable -Name 'WinProfileOps_ProfileFolderPath' + $RegistryHive = Test-EnvironmentVariable -Name 'WinProfileOps_RegistryHive' # Resolve SIDs if Usernames are provided if ($PSCmdlet.ParameterSetName -eq 'UserNameSet') @@ -126,17 +127,26 @@ function Remove-UserProfilesFromRegistry $SIDs = $profileGroup.Group.GetEnumerator().SID $profileCount = $profileGroup.Count - # Call the confirmation prompt and skip this group if the user does not confirm - if (-not (PromptForConfirmation -ComputerName $thisComputerName -ItemCount $profileCount -AuditOnly:$AuditOnly -Context $PSCmdlet -confirm:$Confirm)) + try { - Write-Verbose "User chose not to continue for $thisComputerName, skipping." - continue + # Call the confirmation prompt and skip this group if the user does not confirm + if (-not (PromptForConfirmation -ComputerName $thisComputerName -ItemCount $profileCount -AuditOnly:$AuditOnly -Context $PSCmdlet -confirm:$Confirm)) + { + Write-Verbose "User chose not to continue for $thisComputerName, skipping." + continue + } + + # Process the profiles for this computer + $SIDs | Invoke-UserProfileRegRemoval -ComputerName $thisComputerName ` + -RegistryPath $RegistryPath -ProfileFolderPath $ProfileFolderPath ` + -RegistryHive $RegistryHive -Force:$Force -AuditOnly:$AuditOnly -Confirm:$Confirm + } + catch + { + # Handle any errors that occur during processing of this computer + Write-Error "Failed to process $thisComputerName. Error: $_.Exception.Message" + continue # Move to the next computer in the loop } - - # Process the profiles for this computer - $SIDs | Invoke-UserProfileRegRemoval -ComputerName $thisComputerName ` - -RegistryPath $RegistryPath -ProfileFolderPath $ProfileFolderPath ` - -RegistryHive $RegistryHive -Force:$Force -AuditOnly:$AuditOnly -Confirm:$Confirm } } diff --git a/source/prefix.ps1 b/source/prefix.ps1 index cc5caf7..d76534e 100644 --- a/source/prefix.ps1 +++ b/source/prefix.ps1 @@ -4,20 +4,20 @@ $windowsIdentity = [Security.Principal.WindowsIdentity]::GetCurrent() $windowsPrincipal = New-Object Security.Principal.WindowsPrincipal($windowsIdentity) $env:WinProfileOps_IsAdmin = $windowsPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -$env:GetSIDProfileInfo_RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" -$env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::LocalMachine +$env:WinProfileOps_RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" +$env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::LocalMachine $env:WinProfileOps_RegBackUpDirectory = "C:\LHStuff\RegBackUp" -$env:GetSIDProfileInfo_ProfileFolderPath = $env:SystemDrive + "\Users" +$env:WinProfileOps_ProfileFolderPath = $env:SystemDrive + "\Users" [scriptblock]$SB = { if (Test-Path Env:\WinProfileOps_IsAdmin) { Remove-Item Env:\WinProfileOps_IsAdmin -errorAction SilentlyContinue - Remove-Item Env:\GetSIDProfileInfo_RegistryPath -ErrorAction SilentlyContinue - Remove-Item Env:\GetSIDProfile_RegistryHive -ErrorAction SilentlyContinue + Remove-Item Env:\WinProfileOps_RegistryPath -ErrorAction SilentlyContinue + Remove-Item Env:\WinProfileOps_RegistryHive -ErrorAction SilentlyContinue Remove-Item Env:\WinProfileOps_RegBackUpDirectory -ErrorAction SilentlyContinue - Remove-Item Env:\GetSIDProfileInfo_ProfileFolderPath -ErrorAction SilentlyContinue + Remove-Item Env:\WinProfileOps_ProfileFolderPath -ErrorAction SilentlyContinue } } diff --git a/tests/Helpers/MockedProfListReg.ps1 b/tests/Helpers/MockedProfListReg.ps1 index 366a103..2c0da97 100644 --- a/tests/Helpers/MockedProfListReg.ps1 +++ b/tests/Helpers/MockedProfListReg.ps1 @@ -42,9 +42,9 @@ if (-not (Test-Path $MockRegistryPath)) } # Set up the environment variable for the registry path -$env:GetSIDProfileInfo_RegistryPath = "Software\Pester\ProfileList" -$env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser -$env:GetSIDProfileInfo_ProfileFolderPath = "$TestDrive\Users" +$env:WinProfileOps_RegistryPath = "Software\Pester\ProfileList" +$env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser +$env:WinProfileOps_ProfileFolderPath = "$TestDrive\Users" # Create registry items for each mock user $MockUsers | ForEach-Object { @@ -61,7 +61,7 @@ $MockUsers | ForEach-Object { $null = Set-ItemProperty -Path $RegistryItemPath -Name ProfileImagePath -Value "$TestDrive\Users\$FolderName" } -$ProfileFolderPath = $env:GetSIDProfileInfo_ProfileFolderPath +$ProfileFolderPath = $env:WinProfileOps_ProfileFolderPath $userProfileAudit = Invoke-UserProfileAudit -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial $out = Remove-UserProfilesFromRegistry -SIDs "S-1-5-21-1234567890-1", "S-1-5-21-1234567890-2" -Confirm:$false diff --git a/tests/Intergration/PublicFunctions.tests.ps1 b/tests/Intergration/PublicFunctions.tests.ps1 index 575a010..66cbf28 100644 --- a/tests/Intergration/PublicFunctions.tests.ps1 +++ b/tests/Intergration/PublicFunctions.tests.ps1 @@ -103,8 +103,8 @@ Describe "PublicFuntions Tests" -Tag "Intergration" { } # Set up the environment variable for the registry path - $env:GetSIDProfileInfo_RegistryPath = "Software\Pester\ProfileList" - $env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser + $env:WinProfileOps_RegistryPath = "Software\Pester\ProfileList" + $env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser # Create registry items for each mock user $MockUsers | ForEach-Object { @@ -136,8 +136,8 @@ Describe "PublicFuntions Tests" -Tag "Intergration" { } #resetEnvVariables - Remove-Item -Path Env:GetSIDProfileInfo_RegistryPath -ErrorAction SilentlyContinue - Remove-Item -Path Env:GetSIDProfile_RegistryHive -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_RegistryPath -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_RegistryHive -ErrorAction SilentlyContinue } It "Should return an array of user profiles from the registry" { @@ -211,8 +211,8 @@ Describe "PublicFuntions Tests" -Tag "Intergration" { } # Set up the environment variable for the registry path - $env:GetSIDProfileInfo_RegistryPath = "Software\Pester\ProfileList" - $env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser + $env:WinProfileOps_RegistryPath = "Software\Pester\ProfileList" + $env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser # Create registry items for each mock user $MockUsers | ForEach-Object { @@ -244,8 +244,8 @@ Describe "PublicFuntions Tests" -Tag "Intergration" { } #resetEnvVariables - Remove-Item -Path Env:GetSIDProfileInfo_RegistryPath -ErrorAction SilentlyContinue - Remove-Item -Path Env:GetSIDProfile_RegistryHive -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_RegistryPath -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_RegistryHive -ErrorAction SilentlyContinue } It "It should return non orphaned Audit Objects" { @@ -315,7 +315,6 @@ Describe "PublicFuntions Tests" -Tag "Intergration" { } } - Context "Get-OrphanedProfiles" { BeforeEach { @@ -356,8 +355,8 @@ Describe "PublicFuntions Tests" -Tag "Intergration" { } # Set up the environment variable for the registry path - $env:GetSIDProfileInfo_RegistryPath = "Software\Pester\ProfileList" - $env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser + $env:WinProfileOps_RegistryPath = "Software\Pester\ProfileList" + $env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser # Create registry items for each mock user $MockUsers | ForEach-Object { @@ -388,8 +387,8 @@ Describe "PublicFuntions Tests" -Tag "Intergration" { } #resetEnvVariables - Remove-Item -Path Env:GetSIDProfileInfo_RegistryPath -ErrorAction SilentlyContinue - Remove-Item -Path Env:GetSIDProfile_RegistryHive -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_RegistryPath -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_RegistryHive -ErrorAction SilentlyContinue } It "Should return null if no orphaned profiles are found" { @@ -456,4 +455,221 @@ Describe "PublicFuntions Tests" -Tag "Intergration" { } } + + Context "Remove-UserProfilesFromRegistry" { + + BeforeEach { + # Ensure clean-up of TestDrive before creating folders + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force -ErrorAction SilentlyContinue + } + + # Create mock profile folders in TestDrive + $MockProfilePath = mkdir "$TestDrive\Users" + $MockUsers = @( + @{ + Foldername = "User1" + SID = "S-1-5-21-1234567890-1" + }, + @{ + Foldername = "User2" + SID = "S-1-5-21-1234567890-2" + }, + @{ + Foldername = "User3" + SID = "S-1-5-21-1234567890-3" + } + ) + + $MockUsers | ForEach-Object { + mkdir "$TestDrive\Users\$($_.Foldername)" + } + + # Mock registry entries in TestRegistry + $MockRegistryPath = "HKCU:\Software\Pester\ProfileList" + + # Create registry path if it doesn't exist + if (-not (Test-Path $MockRegistryPath)) + { + New-Item -Path $MockRegistryPath -ItemType Directory + } + + # Set up the environment variable for the registry path + $env:WinProfileOps_RegistryPath = "Software\Pester\ProfileList" + $env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::CurrentUser + $env:WinProfileOps_ProfileFolderPath = "$TestDrive\Users" + + # Create registry items for each mock user + $MockUsers | ForEach-Object { + $SID = $_.SID + $FolderName = $_.Foldername + $RegistryItemPath = "$MockRegistryPath\$SID" + + # Create registry key and set profile path + if (-not (Test-Path $RegistryItemPath)) + { + New-Item -Path $RegistryItemPath + } + + Set-ItemProperty -Path $RegistryItemPath -Name ProfileImagePath -Value "$TestDrive\Users\$FolderName" + } + } + + AfterEach { + # Clean up mock folders and registry items + if (Test-Path "$TestDrive\Users") + { + Remove-Item -Path "$TestDrive\Users" -Recurse -Force + } + + if (Test-Path "HKCU:\Software\Pester\ProfileList") + { + Remove-Item -Path "HKCU:\Software\Pester\ProfileList" -Recurse + } + + # Reset environment variables + Remove-Item -Path Env:WinProfileOps_RegistryPath -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_RegistryHive -ErrorAction SilentlyContinue + Remove-Item -Path Env:WinProfileOps_ProfileFolderPath -ErrorAction SilentlyContinue + } + + It "Should remove the specified profiles from the registry" { + $testSID = "S-1-5-21-1234567890-1" + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Validate the registry entry exists before removal + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $true + + # Call Remove-UserProfilesFromRegistry to remove the profile + $result = Remove-UserProfilesFromRegistry -SIDs $testSID -Force -Confirm:$false + + # Validate that the profile was removed from the registry + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $false + $result.DeletionSuccess | Should -Be $true + } + + It "Should not remove profiles in AuditOnly mode" { + $testSID = "S-1-5-21-1234567890-1" + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Validate the registry entry exists before attempting audit + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $true + + # Call Remove-UserProfilesFromRegistry with -AuditOnly + $result = Remove-UserProfilesFromRegistry -SIDs $testSID -AuditOnly + + # Validate that the profile was not removed + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $true + $result | Should -Not -BeNullOrEmpty + $result.DeletionMessage | Should -Be "Audit only, no deletion performed." + } + + It "Should handle missing registry entries gracefully" { + $missingSID = "S-1-5-21-1234567890-999" # Non-existing SID + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Call Remove-UserProfilesFromRegistry on a missing profile + $result = Remove-UserProfilesFromRegistry -SIDs $missingSID -Force -Confirm:$false + + # Validate the result should indicate failure due to missing registry entry + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be "Profile not found" + } + + It "Should remove multiple profiles from the registry" { + $testSIDs = @("S-1-5-21-1234567890-1", "S-1-5-21-1234567890-2") + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Validate the registry entries exist before removal + $testSIDs | ForEach-Object { + (Test-Path "HKCU:\Software\Pester\ProfileList\$_") | Should -Be $true + } + + # Call Remove-UserProfilesFromRegistry to remove the profiles + $result = Remove-UserProfilesFromRegistry -SIDs $testSIDs -Force -Confirm:$false + + # Validate that the profiles were removed from the registry + $testSIDs | ForEach-Object { + (Test-Path "HKCU:\Software\Pester\ProfileList\$_") | Should -Be $false + } + + $result.DeletionSuccess | ForEach-Object { $_ | Should -Be $true } + } + + It "Should handle profiles with no registry entries" { + $testSID = "S-1-5-21-1234567890-1" + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Remove registry entry for the test profile + Remove-Item "HKCU:\Software\Pester\ProfileList\$testSID" -Recurse -Force + + # Call Remove-UserProfilesFromRegistry + $result = Remove-UserProfilesFromRegistry -SIDs $testSID -Force -Confirm:$false + + # Validate result indicates no registry entry found + $result.DeletionSuccess | Should -Be $false + $result.DeletionMessage | Should -Be "Profile not found" + } + + It "Should handle profiles with missing folders" { + $testSID = "S-1-5-21-1234567890-1" + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + # Remove folder for the profile + Remove-Item "$profilePath\User1" -Recurse -Force + + # Call Remove-UserProfilesFromRegistry + $result = Remove-UserProfilesFromRegistry -SIDs $testSID -Force -Confirm:$false + + # Validate the registry entry was removed + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $false + $result.DeletionSuccess | Should -Be $true + } + + It "Should prompt for confirmation before removing profiles" { + $testSID = "S-1-5-21-1234567890-1" + $profilePath = "$TestDrive\Users" + $ComputerName = $Env:COMPUTERNAME + + InModuleScope -ScriptBlock { + mock ShouldContinueWrapper { + $false + } + } + + # Call Remove-UserProfilesFromRegistry without confirming + $result = Remove-UserProfilesFromRegistry -SIDs $testSID -Force -Confirm:$true + + # assert ShouldContinueWrapper was called + Assert-MockCalled ShouldContinueWrapper -Exactly 1 -Scope It + + # Validate that the profile was not removed + (Test-Path "HKCU:\Software\Pester\ProfileList\$testSID") | Should -Be $true + } + + It "Should handel UserProfile Types from the Pipeline" { + $computerName = $Env:COMPUTERNAME + $userProfileAudit = Invoke-UserProfileAudit -ProfileFolderPath $env:WinProfileOps_ProfileFolderPath -IgnoreSpecial + + if ($userProfileAudit.count -eq 3) + { + $result = Remove-UserProfilesFromRegistry -UserProfiles $userProfileAudit -AuditOnly + } + + $result | Should -Not -BeNullOrEmpty + $result.count | Should -Be 3 + $result | ForEach-Object { + $_.DeletionSuccess | Should -Be $true + } + + } + + } } diff --git a/tests/Unit/Private/Remove-RegistryKeyForSID.tests.ps1 b/tests/Unit/Private/Remove-RegistryKeyForSID.tests.ps1 deleted file mode 100644 index 823c548..0000000 --- a/tests/Unit/Private/Remove-RegistryKeyForSID.tests.ps1 +++ /dev/null @@ -1,111 +0,0 @@ -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 - } - } - } -} diff --git a/tests/Unit/Private/Remove-SIDProfile.tests.ps1 b/tests/Unit/Private/Remove-SIDProfile.tests.ps1 deleted file mode 100644 index 590f5e4..0000000 --- a/tests/Unit/Private/Remove-SIDProfile.tests.ps1 +++ /dev/null @@ -1,146 +0,0 @@ -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 - } - } - } -} diff --git a/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 b/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 index aa7c22a..93edc65 100644 --- a/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 +++ b/tests/Unit/Public/Remove-UserProfilesFromRegistry.tests.ps1 @@ -8,10 +8,10 @@ BeforeAll { $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName # Set up environment variables used in the function - $env:GetSIDProfileInfo_RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" - $env:GetSIDProfile_RegistryHive = [Microsoft.Win32.RegistryHive]::LocalMachine + $env:WinProfileOps_RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + $env:WinProfileOps_RegistryHive = [Microsoft.Win32.RegistryHive]::LocalMachine $env:WinProfileOps_RegBackUpDirectory = "C:\LHStuff\RegBackUp" - $env:GetSIDProfileInfo_ProfileFolderPath = "$env:SystemDrive\Users" + $env:WinProfileOps_ProfileFolderPath = "$env:SystemDrive\Users" } AfterAll { @@ -763,11 +763,18 @@ Describe 'Remove-UserProfilesFromRegistry' -Tag 'Public' { $MockUserProfileObjects = @() $MockUserProfileObjects += New-UserProfileObject -SID 'S-1-5-21-1234567890-1001' -ProfilePath 'C:\Users\testuser' -IsOrphaned $false -ComputerName 'TestComputer' -IsSpecial:$false + mock Write-Error + # Mock Invoke-UserProfileRegRemoval to throw an error Mock -CommandName Invoke-UserProfileRegRemoval -MockWith { throw "Test exception" } # Run the function and catch the error - { Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force } | Should -Throw "Test exception" + { Remove-UserProfilesFromRegistry -UserProfiles $MockUserProfileObjects -Force } | Should -not -Throw + + + # Assert that Write-Error was called + Assert-MockCalled -CommandName Write-Error -Exactly -Times 1 -Scope It + # Optionally, check if the function handled the error gracefully # Assert that an appropriate error message is logged or returned From dc643271d922ba58809f414b046e80bd4fbc24fa Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Tue, 24 Sep 2024 00:31:09 -0700 Subject: [PATCH 22/23] updated changelog, readme, and fixed tests --- CHANGELOG.md | 36 +++++- README.md | 103 ++++++++++++------ .../Get-SIDProfileInfoFallback.ps1 | 0 .../Get-SIDProfileInfoFallback.tests.ps1 | 0 .../Invoke-UserProfileRegRemoval.tests.ps1 | 8 +- 5 files changed, 104 insertions(+), 43 deletions(-) rename source/{Private => NotImplemented}/Get-SIDProfileInfoFallback.ps1 (100%) rename {tests/Unit/Private => source/NotImplemented}/Get-SIDProfileInfoFallback.tests.ps1 (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index be6ec12..9cbe36a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ FolderName variable ### Added +#### Functions + - New helper function `Validate-SIDFormat` to verify SID value upon retrieval in `Get-ProfilePathFromSID` @@ -28,9 +30,37 @@ in `Get-ProfilePathFromSID` - Registered an `OnRemove` script block and a `PowerShell.Exiting` event to ensure cleanup of the environment variable on module removal or session exit. -- **Get-SIDProfileInfoFallback**: Introduced a new fallback function - `Get-SIDProfileInfoFallback` that retrieves non-special user profile - information using the CIM/WMI method. +- **Remove-UserProfilesFromRegistry**: Added a new function to remove user profiles + from the Windows registry based on SIDs, Usernames, or UserProfile objects. + + - Supports three parameter sets: `UserProfileSet`, `SIDSet`, and `UserNameSet`. + + - Can be run in `AuditOnly` mode, where no actual deletion is performed, or + in deletion mode where profiles are removed. + + - Includes a `Force` switch to bypass confirmation prompts and a `ComputerName` + parameter for targeting remote computers. + + - Graceful error handling and logging for cases where the registry key cannot + be opened or profiles cannot be processed for specific computers. + +#### Environment Variables + +- **`$env:WinProfileOps_IsAdmin`**: A boolean value that determines if the +current user has administrative privileges. This is set by checking the user’s +security role against the built-in Administrator group using Windows security principals. + +- **`$env:WinProfileOps_RegistryPath`**: Specifies the registry path used to + manage user profiles. Default value: `"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"`. + +- **`$env:WinProfileOps_RegistryHive`**: Defines the registry hive to use, + which is set to `LocalMachine` by default. + +- **`$env:WinProfileOps_RegBackUpDirectory`**: Specifies the directory where +registry backups are stored. Default value: `"C:\LHStuff\RegBackUp"`. + +- **`$env:WinProfileOps_ProfileFolderPath`**: The profile folder path, +defaulting to the system drive's `Users` folder. Example: `"C:\Users"`. ### Changed diff --git a/README.md b/README.md index 78f772c..7823b75 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,18 @@ computers. - **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. + - **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. @@ -47,14 +52,17 @@ computers. ## Typical Use Cases -- **Cleaning up orphaned profiles** after system migrations, user deactivations, or - profile corruption. +- **Cleaning up orphaned profiles** after system migrations, +user deactivations, or profile corruption. + - **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. + +- **System maintenance routines** that include profile validation + and management as part of a broader system health check. --- @@ -71,9 +79,9 @@ You have two options to install **WinProfileOps**: Install-Module -Name WinProfileOps ``` -2. **Install from GitHub Releases** +1. **Install from GitHub Releases** You can also download the latest release from the - [GitHub Releases page](https://github.com/LarryWisherMan/WinProfileOps/releases). + [GitHub Releases page](https://github.com/LarryWisherMan/WinProfileOps/releases). Download the `.zip` file, extract it, and place it in one of your `$PSModulePath` directories. @@ -116,7 +124,8 @@ This retrieves user profiles from the registry on `LocalHost`. #### Example 4: Auditing User Profiles -Use the `Invoke-UserProfileAudit` function to audit profiles across the file system and +Use the `Invoke-UserProfileAudit` function to audit profiles across the file + system and registry: ```powershell @@ -126,6 +135,36 @@ $allProfiles = Invoke-UserProfileAudit -ComputerName "Server01" This audits user profiles on `Server01`, returning both file system and registry profile information. +#### Example 5: Removing User Profiles from the Registry + +Use the `Remove-UserProfilesFromRegistry` function to remove user profiles from + the Windows registry based on SIDs, Usernames, or UserProfile objects: + +- Remove profiles by SIDs: + + ```powershell + Remove-UserProfilesFromRegistry -SIDs "S-1-5-21-1234567890-1", "S-1-5-21-1234567890-2" + ``` + +- Remove profiles by usernames on a remote computer: + + ```powershell + Remove-UserProfilesFromRegistry -Usernames "john.doe", "jane.smith" + -ComputerName "Server01" -Force -Confirm:$false + ``` + +- Audit user profiles before removal: + + ```powershell + Remove-UserProfilesFromRegistry -UserProfiles $userProfileList -AuditOnly + ``` + +**Note:** To bypass any confirmation prompts during profile removal, both the + `-Force` switch and `-Confirm:$false` must be specified. + +This allows you to either remove or audit profiles based on their SIDs, +usernames, or UserProfile objects. + --- ## Key Functions @@ -138,41 +177,33 @@ profile information. registry. - **`Get-UserProfilesFromFolders`**: Retrieves user profile folders from the file system. +- **`Remove-UserProfilesFromRegistry`**: Removes user profiles from the Windows +registry based on SIDs, Usernames, or UserProfile objects, + with options for audit-only mode or forced removal. --- -## Upcoming Features - -### `Remove-UserProfile` (Coming Soon!) +## Environment Variables -The `Remove-UserProfile` function will provide the ability to remove user -profiles safely from both the registry and the file system. Here are some of the - key features being tested for this functionality: +The **WinProfileOps** module uses several environment variables to configure +certain default paths and behaviors. These variables are automatically set +when the module is loaded and can be adjusted as needed: -- **Safely remove user profiles** either from the file system (i.e., user profile - folders) or from the Windows registry. +- **`$env:WinProfileOps_IsAdmin`**: Determines if the current user has +administrative privileges. It is determined by the current context of the +user. + +- **`$env:WinProfileOps_RegistryPath`**: Specifies the registry path used for + managing user profiles. Default value: `"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"`. -- **Flexible inputs**: Accepts a UserProfile object, a username, or a SID for - profile removal. +- **`$env:WinProfileOps_RegistryHive`**: Defines the registry hive used in + operations, set to `LocalMachine` by default. -- **Powerful safeguards**: Uses `ShouldProcess`, `-WhatIf`, and `-Confirm` to - ensure that deletion is intentional and carefully reviewed. - -- **Handles special accounts**: Prevents accidental removal of critical system - or service accounts (e.g., `S-1-5-18`). - -- **Remote profile removal**: Enables profile deletion from both local and remote - computers. - -- **Verbose and error handling**: Logs every action taken and handles errors to - provide full transparency and avoid unexpected issues. - -The feature is being heavily tested to ensure safety, reliability, and accuracy. - ---- +- **`$env:WinProfileOps_RegBackUpDirectory`**: Specifies the directory where + registry backups are stored. Default value: `"C:\LHStuff\RegBackUp"`. -## Contributing +- **`$env:WinProfileOps_ProfileFolderPath`**: The profile folder path, defaulting + to `"C:\Users"`, but can be customized based on the system's configuration. -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. +These variables are set automatically when the module is imported and are cleared + when the module is unloaded or the PowerShell session ends. diff --git a/source/Private/Get-SIDProfileInfoFallback.ps1 b/source/NotImplemented/Get-SIDProfileInfoFallback.ps1 similarity index 100% rename from source/Private/Get-SIDProfileInfoFallback.ps1 rename to source/NotImplemented/Get-SIDProfileInfoFallback.ps1 diff --git a/tests/Unit/Private/Get-SIDProfileInfoFallback.tests.ps1 b/source/NotImplemented/Get-SIDProfileInfoFallback.tests.ps1 similarity index 100% rename from tests/Unit/Private/Get-SIDProfileInfoFallback.tests.ps1 rename to source/NotImplemented/Get-SIDProfileInfoFallback.tests.ps1 diff --git a/tests/Unit/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.tests.ps1 b/tests/Unit/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.tests.ps1 index 78b473d..8e26ffe 100644 --- a/tests/Unit/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.tests.ps1 +++ b/tests/Unit/Private/RemoveProfileReg/Invoke-UserProfileRegRemoval.tests.ps1 @@ -43,7 +43,7 @@ Describe 'Invoke-UserProfileRegRemoval' -Tags 'Private', 'UserProfileReg' { # Mock profile audit results using New-UserProfileObject Mock Invoke-UserProfileAudit { - $mockAuditResults = @() + $mockAuditResults = [UserProfile[]]@() $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12346' -ProfilePath 'C:\Users\Test2' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false return $mockAuditResults @@ -93,7 +93,7 @@ Describe 'Invoke-UserProfileRegRemoval' -Tags 'Private', 'UserProfileReg' { # Mock profile audit results using New-UserProfileObject Mock Invoke-UserProfileAudit { - $mockAuditResults = @() + $mockAuditResults = [UserProfile[]]@() $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12346' -ProfilePath 'C:\Users\Test2' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false return $mockAuditResults @@ -123,7 +123,7 @@ Describe 'Invoke-UserProfileRegRemoval' -Tags 'Private', 'UserProfileReg' { # Mock profile audit results using New-UserProfileObject Mock Invoke-UserProfileAudit { - $mockAuditResults = @() + $mockAuditResults = [UserProfile[]]@() $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false return $mockAuditResults } @@ -177,7 +177,7 @@ Describe 'Invoke-UserProfileRegRemoval' -Tags 'Private', 'UserProfileReg' { # Mock profile audit results using New-UserProfileObject Mock Invoke-UserProfileAudit { - $mockAuditResults = @() + $mockAuditResults = [UserProfile[]]@() $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12345' -ProfilePath 'C:\Users\Test1' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false $mockAuditResults += New-UserProfileObject -SID 'S-1-5-21-12346' -ProfilePath 'C:\Users\Test2' -IsOrphaned $false -ComputerName 'Server01' -IsSpecial $false return $mockAuditResults From 7c818aad10be801af35ffac9cc253002874f232a Mon Sep 17 00:00:00 2001 From: LarryWisherMan Date: Tue, 24 Sep 2024 00:58:30 -0700 Subject: [PATCH 23/23] updated changelog due to parsing error --- CHANGELOG.md | 92 +++++++++++++++++++++++++--------------------------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cbe36a..98ad0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,87 +7,87 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- removed bug from `Process-RegistryProfiles` regarding populating the -FolderName variable +- Removed bug from `Process-RegistryProfiles` regarding populating the `FolderName` +variable. ### Added #### Functions -- New helper function `Validate-SIDFormat` to verify SID value upon retrieval -in `Get-ProfilePathFromSID` - -- **Admin Detection and Environment Variable**: Added logic to detect whether the - current user is an administrator and set an environment variable - `WinProfileOps_IsAdmin` accordingly. - - - If the user is an administrator, `$ENV:WinProfileOps_IsAdmin` is set to - `$true`. If not, it's set to `$false`. - - - The environment variable is automatically removed when the module is - unloaded or when PowerShell exits. +- New helper function `Validate-SIDFormat` to verify SID value upon retrieval in +`Get-ProfilePathFromSID`. +- **Admin Detection and Environment Variable**: Added logic to detect whether +the current user is an administrator and set an environment variable +`WinProfileOps_IsAdmin` accordingly. + + - If the user is an administrator, `$env:WinProfileOps_IsAdmin` is set to + `$true`. If not, it's set to `$false`. + + - The environment variable is automatically removed when the module is unloaded + or when PowerShell exits. + - Registered an `OnRemove` script block and a `PowerShell.Exiting` event to - ensure cleanup of the environment variable on module removal or session exit. + ensure cleanup of the environment variable on module removal or session exit. - **Remove-UserProfilesFromRegistry**: Added a new function to remove user profiles - from the Windows registry based on SIDs, Usernames, or UserProfile objects. - +from the Windows registry based on SIDs, Usernames, or UserProfile objects. + - Supports three parameter sets: `UserProfileSet`, `SIDSet`, and `UserNameSet`. - + - Can be run in `AuditOnly` mode, where no actual deletion is performed, or - in deletion mode where profiles are removed. - - - Includes a `Force` switch to bypass confirmation prompts and a `ComputerName` - parameter for targeting remote computers. - + in deletion mode where profiles are removed. + + - Includes a `Force` switch to bypass confirmation prompts and a + `ComputerName` parameter for targeting remote computers. + - Graceful error handling and logging for cases where the registry key cannot - be opened or profiles cannot be processed for specific computers. + be opened or profiles cannot be processed for specific computers. -#### Environment Variables +#### Environment Variables + +- **`$env:WinProfileOps_IsAdmin`**: A boolean value that determines if the current + user has administrative privileges. This is set by checking the user's security + role against the built-in Administrator group using Windows security principals. -- **`$env:WinProfileOps_IsAdmin`**: A boolean value that determines if the -current user has administrative privileges. This is set by checking the user’s -security role against the built-in Administrator group using Windows security principals. - - **`$env:WinProfileOps_RegistryPath`**: Specifies the registry path used to manage user profiles. Default value: `"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"`. -- **`$env:WinProfileOps_RegistryHive`**: Defines the registry hive to use, - which is set to `LocalMachine` by default. +- **`$env:WinProfileOps_RegistryHive`**: Defines the registry hive to use, which + is set to `LocalMachine` by default. - **`$env:WinProfileOps_RegBackUpDirectory`**: Specifies the directory where -registry backups are stored. Default value: `"C:\LHStuff\RegBackUp"`. + registry backups are stored. Default value: `"C:\LHStuff\RegBackUp"`. -- **`$env:WinProfileOps_ProfileFolderPath`**: The profile folder path, -defaulting to the system drive's `Users` folder. Example: `"C:\Users"`. +- **`$env:WinProfileOps_ProfileFolderPath`**: The profile folder path, defaulting + to the system drive's `Users` folder. Example: `"C:\Users"`. ### Changed - **Get-UserProfilesFromRegistry**: Updated the function to handle scenarios where the current user does not have administrative privileges. - + - The function now checks if the user is an administrator by evaluating the `WinProfileOps_IsAdmin` environment variable. - + - If the user has administrator privileges, the function retrieves user profiles from the registry using `Get-SIDProfileInfo`. - + - If the user lacks administrative privileges, the function falls back to the `Get-SIDProfileInfoFallback` method, which retrieves user profiles using CIM/WMI without requiring registry access. - - - A warning is logged when the fallback method is used, indicating that - special system accounts are excluded. + + - A warning is logged when the fallback method is used, indicating that special + system accounts are excluded. - Refactored `Process-RegistryProfiles` to better account for access denied errors -when testing profile paths with `Test-FolderExists` + when testing profile paths with `Test-FolderExists`. - Updated `UserProfile` object creation in `Test-OrphanedProfile` for - `$AccessError` Scenarios + `$AccessError` scenarios. -- Module is now using `WinRegOps` Version `0.4.0` for more refined registry value -retrieval +- The module is now using `WinRegOps` version `0.4.0` for more refined registry + value retrieval. ## [0.2.0] - 2024-09-12 @@ -132,7 +132,7 @@ retrieval - These supporting functions are now utilized within `Invoke-UserProfileAudit` to audit user profiles from both the file system and registry sources. - - **`Process-RegistryProfiles`**: + - **`Process-RegistryProfiles`**: - Processes profiles retrieved from the registry, compares them with folder profiles, and identifies orphaned profiles. @@ -163,7 +163,6 @@ an internal function for `Get-RegistryUserProfiles` - Optimized function behavior to handle scenarios with no SIDs, invalid SID formats, and missing `ProfileImagePath` values gracefully. - - **`Get-UserFolders`** - The function now logs errors when folder retrieval fails, improving diagnostic feedback. @@ -208,7 +207,6 @@ an internal function for `Get-RegistryUserProfiles` - Returns an empty array `@()` when an error occurs while accessing the user folders, logging an error message. - - **`Invoke-UserProfileAudit`** - Renamed the previous `Get-AllUserProfiles` function to `Invoke-UserProfileAudit`. - Added `Get-AllUserProfiles` as an alias for `Invoke-UserProfileAudit`