diff --git a/.vscode/settings.json b/.vscode/settings.json index 8bf1c69..07fee91 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,6 +16,9 @@ "files.trimTrailingWhitespace": true, "files.trimFinalNewlines": true, "files.insertFinalNewline": true, + "files.autoSave": "afterDelay", // Enables autosave after a delay + "files.autoSaveDelay": 1000, // Autosave after 1 second of inactivity (1000ms) + "editor.formatOnSave": true, // Enables format on save "files.associations": { "*.ps1xml": "xml" }, @@ -37,5 +40,8 @@ "[markdown]": { "files.trimTrailingWhitespace": false, "files.encoding": "utf8" - } + }, + "conventionalCommits.scopes": [ + "WinProfileOps" + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 251c9ad..39412e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,25 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- For new features. - -### Changed - -- For changes in existing functionality. - -### Deprecated - -- For soon-to-be removed features. - -### Removed - -- For now removed features. - -### Fixed - -- For any bug fix. - -### Security - -- In case of vulnerabilities. - +- Added core functions +- configured `WinRegOps` as a dependant module +- Updated build file for release diff --git a/README.md b/README.md index deb9a59..c2780d4 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,3 @@ Typical use cases include: - Managing user profiles in large-scale, multi-user environments (e.g., terminal servers, Citrix environments). - Excluding system accounts from profile cleanup operations, ensuring important profiles remain intact. - Providing profile management capabilities as part of system maintenance routines. - -## Make it yours - ---- -Generated with Plaster and the SampleModule template - - -This is a sample Readme - -## Make it yours diff --git a/RequiredModules.psd1 b/RequiredModules.psd1 index 618c80b..dffe68b 100644 --- a/RequiredModules.psd1 +++ b/RequiredModules.psd1 @@ -12,14 +12,21 @@ # } #} - InvokeBuild = 'latest' - PSScriptAnalyzer = 'latest' - Pester = 'latest' - ModuleBuilder = 'latest' - ChangelogManagement = 'latest' - Sampler = 'latest' - 'Sampler.GitHubTasks' = 'latest' - 'WisherTools.Helpers' = 'latest' + InvokeBuild = 'latest' + PSScriptAnalyzer = 'latest' + Pester = 'latest' + ModuleBuilder = 'latest' + ChangelogManagement = 'latest' + Sampler = 'latest' + 'Sampler.GitHubTasks' = 'latest' + #'WisherTools.Helpers' = 'latest' + 'WinRegOps' = @{ + Version = '0.3.0-preview0003' + Parameters = @{ + AllowPrerelease = $true + Repository = "PSGallery" + } + } } diff --git a/build.yaml b/build.yaml index 204263d..279f478 100644 --- a/build.yaml +++ b/build.yaml @@ -5,12 +5,12 @@ # Path to the Module Manifest to build (where path will be resolved from) # SourcePath: ./Sampler/Sampler.psd1 # Output Directory where ModuleBuilder will build the Module, relative to module manifest -# OutputDirectory: ../output/Sampler +OutputDirectory: ../output/module BuiltModuleSubdirectory: module CopyPaths: - en-US # - DSCResources - # - Modules +# - Modules Encoding: UTF8 # Can be used to manually specify module's semantic version if the preferred method of # using GitVersion is not available, and it is not possible to set the session environment @@ -28,6 +28,13 @@ VersionedOutputDirectory: true #################################################### NestedModule: + WinRegOps: # This is the first submodule to build into the output + CopyOnly: true + Path: ./output/RequiredModules/WinRegOps + AddToManifest: True + + # # is trimmed (remove metadata & Prerelease tag) and OutputDirectory expanded (the only one) + # VersionedOutputDirectory: false # HelperSubmodule: # This is the first submodule to build into the output # Path: ./*/Modules/HelperSubmodule/HelperSubmodule.psd1 # # is trimmed (remove metadata & Prerelease tag) and OutputDirectory expanded (the only one) @@ -43,7 +50,7 @@ NestedModule: #################################################### # Defining 'Workflows' (suite of InvokeBuild tasks) to be run using their alias BuildWorkflow: - '.': # "." is the default Invoke-Build workflow. It is called when no -Tasks is specified to the build.ps1 + ".": # "." is the default Invoke-Build workflow. It is called when no -Tasks is specified to the build.ps1 - build - test @@ -53,13 +60,10 @@ BuildWorkflow: - Build_NestedModules_ModuleBuilder - Create_changelog_release_output - pack: - build - package_module_nupkg - - # Defining test task to be run when invoking `./build.ps1 -Tasks test` test: # Uncomment to modify the PSModulePath in the test pipeline (also requires the build configuration section SetPSModulePath). @@ -73,10 +77,9 @@ BuildWorkflow: # Use this task when you have multiple parallel tests, which produce multiple # code coverage files and needs to get merged into one file. #merge: - #- Merge_CodeCoverage_Files + #- Merge_CodeCoverage_Files publish: - - Publish_Release_To_GitHub # Runs first, if token is expired it will fail early - publish_module_to_gallery @@ -88,6 +91,7 @@ Pester: OutputFormat: NUnitXML # Excludes one or more paths from being used to calculate code coverage. ExcludeFromCodeCoverage: + - Modules/WinRegOps # If no scripts are defined the default is to use all the tests under the project's # tests folder or source folder (if present). Test script paths can be defined to @@ -117,9 +121,8 @@ Pester: # CodeCoverageMergedOutputFile - the file that is created by the merge build task and # is the file that should be uploaded to code coverage services. #CodeCoverage: - #CodeCoverageFilePattern: JaCoCo_Merge.xml # the pattern used to search all pipeline test job artifacts - #CodeCoverageMergedOutputFile: JaCoCo_coverage.xml # the file that is created for the merged code coverage - +#CodeCoverageFilePattern: JaCoCo_Merge.xml # the pattern used to search all pipeline test job artifacts +#CodeCoverageMergedOutputFile: JaCoCo_coverage.xml # the file that is created for the merged code coverage # Import ModuleBuilder tasks from a specific PowerShell module using the build # task's alias. Wildcard * can be used to specify all tasks that has a similar @@ -127,10 +130,9 @@ Pester: # module in the file RequiredModules.psd1. ModuleBuildTasks: Sampler: - - '*.build.Sampler.ib.tasks' + - "*.build.Sampler.ib.tasks" Sampler.GitHubTasks: - - '*.ib.tasks' - + - "*.ib.tasks" # Invoke-Build Header to be used to 'decorate' the terminal output of the tasks. TaskHeader: | @@ -143,3 +145,10 @@ TaskHeader: | Write-Build DarkGray " $Path" Write-Build DarkGray " $($Task.InvocationInfo.ScriptName):$($Task.InvocationInfo.ScriptLineNumber)" "" + +GitHubConfig: + GitHubFilesToAdd: + - "CHANGELOG.md" + GitHubConfigUserName: LarryWisherMan + GitHubConfigUserEmail: MikeRow@upkep.net + UpdateChangelogOnPrerelease: false diff --git a/source/Classes/1.class1.ps1 b/source/Classes/1.class1.ps1 deleted file mode 100644 index 22cc7da..0000000 --- a/source/Classes/1.class1.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -class Class1 -{ - [string]$Name = 'Class1' - - Class1() - { - #default Constructor - } - - [String] ToString() - { - # Typo "calss" is intentional - return ( 'This calss is {0}' -f $this.Name) - } -} diff --git a/source/Classes/2.class2.ps1 b/source/Classes/2.class2.ps1 deleted file mode 100644 index 801c626..0000000 --- a/source/Classes/2.class2.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -class Class2 -{ - [string]$Name = 'Class2' - - Class2() - { - #default constructor - } - - [String] ToString() - { - return ( 'This calss is {0}' -f $this.Name) - } -} diff --git a/source/Classes/3.class11.ps1 b/source/Classes/3.class11.ps1 deleted file mode 100644 index a8202a3..0000000 --- a/source/Classes/3.class11.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -class Class11 : Class1 -{ - [string]$Name = 'Class11' - - Class11 () - { - } - - [String] ToString() - { - return ( 'This calss is {0}:{1}' -f $this.Name,'class1') - } -} diff --git a/source/Classes/4.class12.ps1 b/source/Classes/4.class12.ps1 deleted file mode 100644 index c04a82d..0000000 --- a/source/Classes/4.class12.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -class Class12 : Class1 -{ - [string]$Name = 'Class12' - - Class12 () - { - } - - [String] ToString() - { - return ( 'This calss is {0}:{1}' -f $this.Name,'class1') - } -} diff --git a/source/Classes/ProfileDeletionResult.ps1 b/source/Classes/ProfileDeletionResult.ps1 new file mode 100644 index 0000000..4f0a198 --- /dev/null +++ b/source/Classes/ProfileDeletionResult.ps1 @@ -0,0 +1,16 @@ +class ProfileDeletionResult { + [string]$SID + [string]$ProfilePath + [bool]$DeletionSuccess + [string]$DeletionMessage + [string]$ComputerName + + # Constructor to initialize the properties + ProfileDeletionResult([string]$sid, [string]$profilePath, [bool]$deletionSuccess, [string]$deletionMessage, [string]$computerName) { + $this.SID = $sid + $this.ProfilePath = $profilePath + $this.DeletionSuccess = $deletionSuccess + $this.DeletionMessage = $deletionMessage + $this.ComputerName = $computerName + } +} diff --git a/source/Classes/UserProfile.ps1 b/source/Classes/UserProfile.ps1 new file mode 100644 index 0000000..cd231f8 --- /dev/null +++ b/source/Classes/UserProfile.ps1 @@ -0,0 +1,18 @@ +class UserProfile { + [string]$SID + [string]$ProfilePath + [bool]$IsOrphaned + [string]$OrphanReason + [string]$ComputerName + [bool]$IsSpecial + + # Constructor to initialize the properties + UserProfile([string]$sid, [string]$profilePath, [bool]$isOrphaned, [string]$orphanReason, [string]$computerName, [bool]$isSpecial) { + $this.SID = $sid + $this.ProfilePath = $profilePath + $this.IsOrphaned = $isOrphaned + $this.OrphanReason = $orphanReason + $this.ComputerName = $computerName + $this.IsSpecial = $isSpecial + } +} diff --git a/source/Private/Get-PrivateFunction.ps1 b/source/Private/Get-PrivateFunction.ps1 deleted file mode 100644 index b4ef195..0000000 --- a/source/Private/Get-PrivateFunction.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -function Get-PrivateFunction -{ - <# - .SYNOPSIS - This is a sample Private function only visible within the module. - - .DESCRIPTION - This sample function is not exported to the module and only return the data passed as parameter. - - .EXAMPLE - $null = Get-PrivateFunction -PrivateData 'NOTHING TO SEE HERE' - - .PARAMETER PrivateData - The PrivateData parameter is what will be returned without transformation. - - #> - [cmdletBinding()] - [OutputType([string])] - param - ( - [Parameter()] - [String] - $PrivateData - ) - - process - { - Write-Output $PrivateData - } - -} diff --git a/source/Public/Get-AllUserProfiles.ps1 b/source/Public/Get-AllUserProfiles.ps1 new file mode 100644 index 0000000..28d35b0 --- /dev/null +++ b/source/Public/Get-AllUserProfiles.ps1 @@ -0,0 +1,70 @@ +function Get-AllUserProfiles { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + [string]$ComputerName = $env:COMPUTERNAME, + + [string]$ProfileFolderPath = "C:\Users", + [switch]$IgnoreSpecial + ) + + # Begin block runs once before processing pipeline input + begin { + # Initialize an array to hold all UserProfile objects across multiple pipeline inputs + $AllProfiles = @() + } + + # Process block runs once for each input object (in case of pipeline) + process { + # Test if the computer is online before proceeding + if (-not (Test-ComputerPing -ComputerName $ComputerName)) { + Write-Warning "Computer '$ComputerName' is offline or unreachable." + return # Skip to the next input in the pipeline + } + + # Get profiles from folders and registry + $UserFolders = Get-UserProfilesFromFolders -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath + $RegistryProfiles = Get-UserProfilesFromRegistry -ComputerName $ComputerName + + # Loop through registry profiles and check for folder existence and ProfileImagePath + foreach ($regProfile in $RegistryProfiles) { + $profilePath = $regProfile.ProfilePath + $folderExists = Test-FolderExists -ProfilePath $profilePath -ComputerName $regProfile.ComputerName + $folderName = Split-Path -Path $profilePath -Leaf + $isSpecial = Test-SpecialAccount -FolderName $folderName -SID $regProfile.SID -ProfilePath $profilePath + + # Skip special profiles if IgnoreSpecial is set + if ($IgnoreSpecial -and $isSpecial) { + continue + } + + # Detect if the profile is orphaned and create the user profile object + $userProfile = Test-OrphanedProfile -SID $regProfile.SID -ProfilePath $profilePath ` + -FolderExists $folderExists -IgnoreSpecial $IgnoreSpecial ` + -IsSpecial $isSpecial -ComputerName $ComputerName + $AllProfiles += $userProfile + } + + # Loop through user folders and check if they exist in the registry + foreach ($folder in $UserFolders) { + $registryProfile = $RegistryProfiles | Where-Object { $_.ProfilePath -eq $folder.ProfilePath } + $isSpecial = Test-SpecialAccount -FolderName $folder.FolderName -SID $null -ProfilePath $folder.ProfilePath + + # Skip special profiles if IgnoreSpecial is set + if ($IgnoreSpecial -and $isSpecial) { + continue + } + + # Case 4: Folder exists in C:\Users but not in the registry + if (-not $registryProfile) { + $AllProfiles += New-UserProfileObject $null $folder.ProfilePath $true "MissingRegistryEntry" $ComputerName $isSpecial + } + } + } + + # End block runs once after all processing is complete + end { + # Output all collected profiles + $AllProfiles + } +} diff --git a/source/Public/Get-OrphanedProfiles.ps1 b/source/Public/Get-OrphanedProfiles.ps1 new file mode 100644 index 0000000..8426042 --- /dev/null +++ b/source/Public/Get-OrphanedProfiles.ps1 @@ -0,0 +1,21 @@ +function Get-OrphanedProfiles { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$ComputerName = $env:COMPUTERNAME, + + [Parameter(Mandatory = $false)] + [string]$ProfileFolderPath = "C:\Users", + + [switch]$IgnoreSpecial + ) + + # Get all user profiles (both registry and filesystem) using the existing function + $allProfiles = Get-AllUserProfiles -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial + + # Filter the profiles to return only orphaned ones + $orphanedProfiles = $allProfiles | Where-Object { $_.IsOrphaned -eq $true } + + # Return the orphaned profiles + return $orphanedProfiles +} diff --git a/source/Public/Get-ProfilePathFromSID.ps1 b/source/Public/Get-ProfilePathFromSID.ps1 new file mode 100644 index 0000000..674b66c --- /dev/null +++ b/source/Public/Get-ProfilePathFromSID.ps1 @@ -0,0 +1,19 @@ +function Get-ProfilePathFromSID { + param ( + [Microsoft.Win32.RegistryKey]$SidKey + ) + + try { + # Use Get-RegistryValue to retrieve the "ProfileImagePath" + $profileImagePath = Get-RegistryValue -Key $SidKey -ValueName "ProfileImagePath" + + if (-not $profileImagePath) { + Write-Verbose "ProfileImagePath not found for SID '$($SidKey.Name)'." + } + + return $profileImagePath + } catch { + Write-Error "Failed to retrieve ProfileImagePath for SID '$($SidKey.Name)'. Error: $_" + return $null + } +} diff --git a/source/Public/Get-RegistryKeyForSID.ps1 b/source/Public/Get-RegistryKeyForSID.ps1 new file mode 100644 index 0000000..2ed0833 --- /dev/null +++ b/source/Public/Get-RegistryKeyForSID.ps1 @@ -0,0 +1,19 @@ +function Get-RegistryKeyForSID { + param ( + [string]$SID, + [Microsoft.Win32.RegistryKey]$ProfileListKey + ) + + try { + # Use the general Open-RegistrySubKey function to get the subkey for the SID + $sidKey = Open-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID + if ($sidKey -eq $null) { + Write-Warning "The SID '$SID' does not exist in the ProfileList registry." + return $null + } + return $sidKey + } catch { + Write-Error "Error accessing registry key for SID '$SID'. Error: $_" + return $null + } +} diff --git a/source/Public/Get-SIDProfileInfo.ps1 b/source/Public/Get-SIDProfileInfo.ps1 new file mode 100644 index 0000000..e0d7b81 --- /dev/null +++ b/source/Public/Get-SIDProfileInfo.ps1 @@ -0,0 +1,37 @@ +function Get-SIDProfileInfo { + [CmdletBinding()] + param ( + [string]$ComputerName = $env:COMPUTERNAME + ) + + $RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName + + if ($ProfileListKey -eq $null) { + Write-Error "Failed to open registry path: $RegistryPath on $ComputerName." + return + } + + $ProfileRegistryItems = foreach ($sid in $ProfileListKey.GetSubKeyNames()) { + # Use Open-RegistrySubKey to get the subkey for the SID + $subKey = Open-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $sid + + if ($subKey -eq $null) { + Write-Warning "Registry key for SID '$sid' could not be opened." + continue + } + + # Use Get-ProfilePathFromSID to get the ProfileImagePath for the SID + $profilePath = Get-ProfilePathFromSID -SidKey $subKey + + # Return a PSCustomObject with SID, ProfilePath, and ComputerName + [PSCustomObject]@{ + SID = $sid + ProfilePath = $profilePath + ComputerName = $ComputerName + ExistsInRegistry = $true + } + } + + return $ProfileRegistryItems +} diff --git a/source/Public/Get-Something.ps1 b/source/Public/Get-Something.ps1 deleted file mode 100644 index 1bc54b7..0000000 --- a/source/Public/Get-Something.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -function Get-Something -{ - <# - .SYNOPSIS - Sample Function to return input string. - - .DESCRIPTION - This function is only a sample Advanced function that returns the Data given via parameter Data. - - .EXAMPLE - Get-Something -Data 'Get me this text' - - - .PARAMETER Data - The Data parameter is the data that will be returned without transformation. - - #> - [cmdletBinding( - SupportsShouldProcess = $true, - ConfirmImpact = 'Low' - )] - param - ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] - [String] - $Data - ) - - process - { - if ($pscmdlet.ShouldProcess($Data)) - { - Write-Verbose ('Returning the data: {0}' -f $Data) - Get-PrivateFunction -PrivateData $Data - } - else - { - Write-Verbose 'oh dear' - } - } -} diff --git a/source/Public/Get-UserFolders.ps1 b/source/Public/Get-UserFolders.ps1 new file mode 100644 index 0000000..397349a --- /dev/null +++ b/source/Public/Get-UserFolders.ps1 @@ -0,0 +1,21 @@ +function Get-UserFolders { + [CmdletBinding()] + param ( + [string]$ComputerName, + [string]$ProfileFolderPath = "C:\Users" + ) + + $IsLocal = ($ComputerName -eq $env:COMPUTERNAME) + $FolderPath = Get-DirectoryPath -BasePath $ProfileFolderPath -ComputerName $ComputerName -IsLocal $IsLocal + + # Get list of all folders in the user profile directory + $ProfileFolders = Get-ChildItem -Path $FolderPath -Directory | ForEach-Object { + [PSCustomObject]@{ + FolderName = $_.Name + ProfilePath = Get-DirectoryPath -basepath $_.FullName -ComputerName $ComputerName -IsLocal $true + ComputerName = $ComputerName + } + } + + return $ProfileFolders +} diff --git a/source/Public/Get-UserProfilesFromFolders.ps1 b/source/Public/Get-UserProfilesFromFolders.ps1 new file mode 100644 index 0000000..1ce28e1 --- /dev/null +++ b/source/Public/Get-UserProfilesFromFolders.ps1 @@ -0,0 +1,11 @@ +function Get-UserProfilesFromFolders +{ + param ( + [string]$ComputerName = $env:COMPUTERNAME, + [string]$ProfileFolderPath = "C:\Users" + ) + + # Get user folders and return them + $UserFolders = Get-UserFolders -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath + return $UserFolders +} diff --git a/source/Public/Get-UserProfilesFromRegistry.ps1 b/source/Public/Get-UserProfilesFromRegistry.ps1 new file mode 100644 index 0000000..c400d1d --- /dev/null +++ b/source/Public/Get-UserProfilesFromRegistry.ps1 @@ -0,0 +1,10 @@ +function Get-UserProfilesFromRegistry +{ + param ( + [string] $ComputerName = $env:COMPUTERNAME + ) + + # Get registry profiles and return them + $RegistryProfiles = Get-SIDProfileInfo -ComputerName $ComputerName + return $RegistryProfiles +} diff --git a/source/Public/New-UserProfileObject.ps1 b/source/Public/New-UserProfileObject.ps1 new file mode 100644 index 0000000..2d697a0 --- /dev/null +++ b/source/Public/New-UserProfileObject.ps1 @@ -0,0 +1,19 @@ +function New-UserProfileObject { + param ( + [string]$SID, + [string]$ProfilePath, + [bool]$IsOrphaned, + [string]$OrphanReason, + [string]$ComputerName, + [bool]$IsSpecial + ) + + return [UserProfile]::new( + $SID, + $ProfilePath, + $IsOrphaned, + $OrphanReason, + $ComputerName, + $IsSpecial + ) +} diff --git a/source/Public/Remove-OrphanedProfiles.ps1 b/source/Public/Remove-OrphanedProfiles.ps1 new file mode 100644 index 0000000..c61ba91 --- /dev/null +++ b/source/Public/Remove-OrphanedProfiles.ps1 @@ -0,0 +1,34 @@ +function Remove-OrphanedProfiles { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory = $true)] + [string]$ComputerName, + + [Parameter(Mandatory = $false)] + [string]$ProfileFolderPath = "C:\Users", + + [switch]$IgnoreSpecial + ) + + # Step 1: Get the list of orphaned profiles + $orphanedProfiles = Get-OrphanedProfiles-ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial + + if (-not $orphanedProfiles) { + Write-Verbose "No orphaned profiles found on $ComputerName." + return + } + + # Step 2: Extract the SIDs of orphaned profiles that exist in the registry + $orphanedSIDs = $orphanedProfiles | Where-Object { $_.SID } | Select-Object -ExpandProperty SID + + if (-not $orphanedSIDs) { + Write-Verbose "No orphaned profiles with valid SIDs found for removal on $ComputerName." + return + } + + # Step 3: Remove profiles for the collected SIDs + $removalResults = Remove-ProfilesForSIDs -SIDs $orphanedSIDs -ComputerName $ComputerName -Confirm:$false + + # Step 4: Return the results of the removal process + return $removalResults +} diff --git a/source/Public/Remove-ProfilesForSIDs.ps1 b/source/Public/Remove-ProfilesForSIDs.ps1 new file mode 100644 index 0000000..6e8faea --- /dev/null +++ b/source/Public/Remove-ProfilesForSIDs.ps1 @@ -0,0 +1,66 @@ +function Remove-ProfilesForSIDs { + #Orchestrates the deletion process for multiple SIDs. + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param ( + [Parameter(Mandatory = $true)] + [string[]]$SIDs, # Accept multiple SIDs as an array + + [Parameter(Mandatory = $false)] + [string]$ComputerName = $env:COMPUTERNAME # Default to local computer + ) + + # Open the ProfileList registry key + $RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName + + if ($ProfileListKey -eq $null) { + Write-Error "Failed to open ProfileList registry path on $ComputerName." + return + } + + $deletionResults = @() + + # Loop through each SID and process deletion + foreach ($sid in $SIDs) { + try { + # Get profile information for the SID + $sidProfileInfo = Get-SIDProfileInfo -SID $sid -ProfileListKey $ProfileListKey + + if (-not $sidProfileInfo.ExistsInRegistry) { + $deletionResults += [ProfileDeletionResult]::new( + $sid, + $null, + $false, + $sidProfileInfo.Message, + $ComputerName + ) + continue + } + + # Process the deletion of the profile for the SID + $deletionResult = Remove-SIDProfile -SID $sid ` + -ProfileListKey $ProfileListKey ` + -ComputerName $ComputerName ` + -ProfilePath $sidProfileInfo.ProfilePath + + $deletionResults += $deletionResult + } catch { + Write-Error "An error occurred while processing SID '$sid'. $_" + + # Add a deletion result indicating failure due to error + $deletionResults += [ProfileDeletionResult]::new( + $sid, + $null, + $false, + "Error occurred while processing SID '$sid'. Error: $_", + $ComputerName + ) + } + } + + # Close the registry key when done + $ProfileListKey.Close() + + # Return the array of deletion results + return $deletionResults +} diff --git a/source/Public/Remove-RegistryKeyForSID.ps1 b/source/Public/Remove-RegistryKeyForSID.ps1 new file mode 100644 index 0000000..84b6b8f --- /dev/null +++ b/source/Public/Remove-RegistryKeyForSID.ps1 @@ -0,0 +1,22 @@ +function Remove-RegistryKeyForSID { + #Deletes a single registry key for a SID. + [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')] + param ( + [Parameter(Mandatory = $true)] + [string]$SID, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryKey]$ProfileListKey, + + [Parameter(Mandatory = $true)] + [string]$ComputerName = $env:COMPUTERNAME + ) + + try { + # Use the general Remove-RegistrySubKey function to delete the SID's subkey + return Remove-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID -ComputerName $ComputerName + } catch { + Write-Error "Failed to remove the profile registry key for SID '$SID' on $ComputerName. Error: $_" + return $false + } +} diff --git a/source/Public/Remove-SIDProfile.ps1 b/source/Public/Remove-SIDProfile.ps1 new file mode 100644 index 0000000..cc46898 --- /dev/null +++ b/source/Public/Remove-SIDProfile.ps1 @@ -0,0 +1,31 @@ +function Remove-SIDProfile { + #Coordinates the registry key deletion and provides a result for a single SID. + [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')] + param ( + [string]$SID, + [Microsoft.Win32.RegistryKey]$ProfileListKey, + [string]$ComputerName, + [string]$ProfilePath + ) + + # Attempt to remove the registry key + $deletionSuccess = Remove-RegistryKeyForSID -SID $SID -ProfileListKey $ProfileListKey -ComputerName $ComputerName + + if ($deletionSuccess) { + return [ProfileDeletionResult]::new( + $SID, + $ProfilePath, + $true, + "Profile registry key for SID '$SID' successfully deleted.", + $ComputerName + ) + } else { + return [ProfileDeletionResult]::new( + $SID, + $ProfilePath, + $false, + "Failed to delete the profile registry key for SID '$SID'.", + $ComputerName + ) + } +} diff --git a/source/Public/Test-FolderExists.ps1 b/source/Public/Test-FolderExists.ps1 new file mode 100644 index 0000000..2d23252 --- /dev/null +++ b/source/Public/Test-FolderExists.ps1 @@ -0,0 +1,10 @@ +function Test-FolderExists { + param ( + [string]$ProfilePath, + [string]$ComputerName + ) + + $IsLocal = $ComputerName -eq $env:COMPUTERNAME + $pathToCheck = Get-DirectoryPath -BasePath $ProfilePath -ComputerName $ComputerName -IsLocal $IsLocal + return Test-Path $pathToCheck +} diff --git a/source/Public/Test-OrphanedProfile.ps1 b/source/Public/Test-OrphanedProfile.ps1 new file mode 100644 index 0000000..db4d4c7 --- /dev/null +++ b/source/Public/Test-OrphanedProfile.ps1 @@ -0,0 +1,20 @@ +function Test-OrphanedProfile { + param ( + [string]$SID, + [string]$ProfilePath, + [bool]$FolderExists, + [bool]$IgnoreSpecial, + [bool]$IsSpecial, + [string]$ComputerName + ) + + if (-not $ProfilePath) { + return New-UserProfileObject $SID "(null)" $true "MissingProfileImagePath" $ComputerName $IsSpecial + } + elseif (-not $FolderExists) { + return New-UserProfileObject $SID $ProfilePath $true "MissingFolder" $ComputerName $IsSpecial + } + else { + return New-UserProfileObject $SID $ProfilePath $false $null $ComputerName $IsSpecial + } +} diff --git a/source/Public/Test-SpecialAccount.ps1 b/source/Public/Test-SpecialAccount.ps1 new file mode 100644 index 0000000..e669979 --- /dev/null +++ b/source/Public/Test-SpecialAccount.ps1 @@ -0,0 +1,26 @@ +function Test-SpecialAccount { + param ( + [string]$FolderName, + [string]$SID, + [string]$ProfilePath + ) + + # List of default or special accounts to ignore + $IgnoredAccounts = @( + "defaultuser0", "DefaultAppPool", "servcm12", "Public", "PBIEgwService", "Default", + "All Users", "win2kpro" + ) + $IgnoredSIDs = @( + "S-1-5-18", # Local System + "S-1-5-19", # Local Service + "S-1-5-20" # Network Service + ) + $IgnoredPaths = @( + "C:\WINDOWS\system32\config\systemprofile", # System profile + "C:\WINDOWS\ServiceProfiles\LocalService", # Local service profile + "C:\WINDOWS\ServiceProfiles\NetworkService" # Network service profile + ) + + # Check if the account is special based on the folder name, SID, or profile path + return ($IgnoredAccounts -contains $FolderName) -or ($IgnoredSIDs -contains $SID) -or ($IgnoredPaths -contains $ProfilePath) +} diff --git a/source/WinProfileOps.psd1 b/source/WinProfileOps.psd1 index 43a87e3..0543017 100644 --- a/source/WinProfileOps.psd1 +++ b/source/WinProfileOps.psd1 @@ -8,29 +8,29 @@ @{ -# Script module or binary module file associated with this manifest. -RootModule = 'WinProfileOps.psm1' + # Script module or binary module file associated with this manifest. + RootModule = 'WinProfileOps.psm1' -# Version number of this module. -ModuleVersion = '0.0.1' + # Version number of this module. + ModuleVersion = '0.0.1' -# Supported PSEditions -# CompatiblePSEditions = @() + # Supported PSEditions + # CompatiblePSEditions = @() -# ID used to uniquely identify this module -GUID = '1abff4b3-dadd-480c-a825-2671dfb7b3bd' + # ID used to uniquely identify this module + GUID = '1abff4b3-dadd-480c-a825-2671dfb7b3bd' -# Author of this module -Author = 'LarryWisherMan' + # Author of this module + Author = 'LarryWisherMan' -# Company or vendor of this module -CompanyName = 'LarryWisherMan' + # Company or vendor of this module + CompanyName = 'LarryWisherMan' -# Copyright statement for this module -Copyright = '(c) 2024 LarryWisherMan. All rights reserved.' + # Copyright statement for this module + Copyright = '(c) 2024 LarryWisherMan. All rights reserved.' -# Description of the functionality provided by this module -Description = 'The WinProfileOps module provides an essential toolkit for managing Windows user profiles across local and remote computers. This module automates complex profile management tasks such as detecting orphaned profiles, validating profile paths, and removing stale or corrupted profiles. It handles both filesystem and registry operations, leveraging its dependency on WinRegOps for registry-related functions. + # Description of the functionality provided by this module + Description = 'The WinProfileOps module provides an essential toolkit for managing Windows user profiles across local and remote computers. This module automates complex profile management tasks such as detecting orphaned profiles, validating profile paths, and removing stale or corrupted profiles. It handles both filesystem and registry operations, leveraging its dependency on WinRegOps for registry-related functions. WinProfileOps integrates with WinRegOps to seamlessly manage profiles by querying, validating, and deleting user profile-related data from the Windows registry. This module is ideal for system administrators seeking to streamline profile management operations, especially in environments with numerous users and computers. @@ -53,105 +53,108 @@ Typical use cases include: - Excluding system accounts from profile cleanup operations, ensuring important profiles remain intact. - Providing profile management capabilities as part of system maintenance routines.' -# Minimum version of the Windows PowerShell engine required by this module -PowerShellVersion = '5.0' + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.1' -# Name of the Windows PowerShell host required by this module -# PowerShellHostName = '' + # Name of the Windows PowerShell host required by this module + # PowerShellHostName = '' -# Minimum version of the Windows PowerShell host required by this module -# PowerShellHostVersion = '' + # Minimum version of the Windows PowerShell host required by this module + # PowerShellHostVersion = '' -# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# DotNetFrameworkVersion = '' + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' -# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# CLRVersion = '' + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # CLRVersion = '' -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' -# Modules that must be imported into the global environment prior to importing this module -RequiredModules = @() + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @('WisherTools.Helpers') -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + NestedModules = @() -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = @() + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = @() + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() -# Variables to export from this module -VariablesToExport = @() + # Variables to export from this module + VariablesToExport = @() -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = @() + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() -# DSC resources to export from this module -DscResourcesToExport = @() + # DSC resources to export from this module + DscResourcesToExport = @() -# List of all modules packaged with this module -# ModuleList = @() + # List of all modules packaged with this module + # ModuleList = @() -# List of all files packaged with this module -# FileList = @() + # List of all files packaged with this module + # FileList = @() -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ - PSData = @{ + PSData = @{ - Prerelease = '' - # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() + Prerelease = '' + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @( + 'WindowsProfiles', + 'ProfileManagement', + 'OrphanedProfiles', + 'RegistryOperations', + 'FilesystemOperations', + 'RemoteManagement', + 'UserProfiles', + 'ProfileCleanup', + 'WindowsRegistry', + 'SystemAdministration', + 'Automation', + 'ProfileValidation', + 'WindowsManagement', + 'ITAdministration', + 'UserProfileTools' + ) - # A URL to the license for this module. - # LicenseUri = '' + # A URL to the license for this module. + LicenseUri = 'https://github.com/LarryWisherMan/WinProfileOps/blob/main/LICENSE' - # A URL to the main website for this project. - # ProjectUri = '' + # A URL to the main website for this project. + ProjectUri = 'https://github.com/LarryWisherMan/WinProfileOps' - # A URL to an icon representing this module. - # IconUri = '' + # A URL to an icon representing this module. + IconUri = 'https://raw.githubusercontent.com/LarryWisherMan/ModuleIcons/main/WinProfileOps.png' - # ReleaseNotes of this module - ReleaseNotes = '' + # ReleaseNotes of this module + ReleaseNotes = '' - } # End of PSData hashtable + } # End of PSData hashtable -} # End of PrivateData hashtable + } # End of PrivateData hashtable -# HelpInfo URI of this module -# HelpInfoURI = '' + # HelpInfo URI of this module + # HelpInfoURI = '' -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' } - - - - - - - - - - - - - diff --git a/tests/Unit/Classes/class1.tests.ps1 b/tests/Unit/Classes/class1.tests.ps1 deleted file mode 100644 index 1237a87..0000000 --- a/tests/Unit/Classes/class1.tests.ps1 +++ /dev/null @@ -1,46 +0,0 @@ -$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path -$ProjectName = (Get-ChildItem $ProjectPath\*\*.psd1 | Where-Object { - ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and - $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop }catch{$false}) } - ).BaseName - -Import-Module $ProjectName - -InModuleScope $ProjectName { - Describe class1 { - Context 'Type creation' { - It 'Has created a type named class1' { - 'class1' -as [Type] | Should -BeOfType [Type] - } - } - - Context 'Constructors' { - It 'Has a default constructor' { - $instance = [class1]::new() - $instance | Should -Not -BeNullOrEmpty - $instance.GetType().Name | Should -Be 'class1' - } - } - - Context 'Methods' { - BeforeEach { - $instance = [class1]::new() - } - - It 'Overrides the ToString method' { - # Typo "calss" is inherited from definition. Preserved here as validation is demonstrative. - $instance.ToString() | Should -Be 'This calss is class1' - } - } - - Context 'Properties' { - BeforeEach { - $instance = [class1]::new() - } - - It 'Has a Name property' { - $instance.Name | Should -Be 'Class1' - } - } - } -} diff --git a/tests/Unit/Classes/class11.tests.ps1 b/tests/Unit/Classes/class11.tests.ps1 deleted file mode 100644 index c51e385..0000000 --- a/tests/Unit/Classes/class11.tests.ps1 +++ /dev/null @@ -1,46 +0,0 @@ -$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path -$ProjectName = (Get-ChildItem $ProjectPath\*\*.psd1 | Where-Object { - ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and - $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop }catch{$false}) } - ).BaseName - -Import-Module $ProjectName - -InModuleScope $ProjectName { - Describe class11 { - Context 'Type creation' { - It 'Has created a type named class11' { - 'class11' -as [Type] | Should -BeOfType [Type] - } - } - - Context 'Constructors' { - It 'Has a default constructor' { - $instance = [class11]::new() - $instance | Should -Not -BeNullOrEmpty - $instance.GetType().Name | Should -Be 'class11' - } - } - - Context 'Methods' { - BeforeEach { - $instance = [class11]::new() - } - - It 'Overrides the ToString method' { - # Typo "calss" is inherited from definition. Preserved here as validation is demonstrative. - $instance.ToString() | Should -Be 'This calss is class11:class1' - } - } - - Context 'Properties' { - BeforeEach { - $instance = [class11]::new() - } - - It 'Has a Name property' { - $instance.Name | Should -Be 'Class11' - } - } - } -} diff --git a/tests/Unit/Classes/class12.tests.ps1 b/tests/Unit/Classes/class12.tests.ps1 deleted file mode 100644 index f6b24dc..0000000 --- a/tests/Unit/Classes/class12.tests.ps1 +++ /dev/null @@ -1,46 +0,0 @@ -$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path -$ProjectName = (Get-ChildItem $ProjectPath\*\*.psd1 | Where-Object { - ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and - $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop }catch{$false}) } - ).BaseName - -Import-Module $ProjectName - -InModuleScope $ProjectName { - Describe class12 { - Context 'Type creation' { - It 'Has created a type named class12' { - 'class12' -as [Type] | Should -BeOfType [Type] - } - } - - Context 'Constructors' { - It 'Has a default constructor' { - $instance = [class12]::new() - $instance | Should -Not -BeNullOrEmpty - $instance.GetType().Name | Should -Be 'class12' - } - } - - Context 'Methods' { - BeforeEach { - $instance = [class12]::new() - } - - It 'Overrides the ToString method' { - # Typo "calss" is inherited from definition. Preserved here as validation is demonstrative. - $instance.ToString() | Should -Be 'This calss is class12:class1' - } - } - - Context 'Properties' { - BeforeEach { - $instance = [class12]::new() - } - - It 'Has a Name property' { - $instance.Name | Should -Be 'Class12' - } - } - } -} diff --git a/tests/Unit/Classes/class2.tests.ps1 b/tests/Unit/Classes/class2.tests.ps1 deleted file mode 100644 index 16d327a..0000000 --- a/tests/Unit/Classes/class2.tests.ps1 +++ /dev/null @@ -1,46 +0,0 @@ -$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path -$ProjectName = (Get-ChildItem $ProjectPath\*\*.psd1 | Where-Object { - ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and - $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop }catch{$false}) } - ).BaseName - -Import-Module $ProjectName - -InModuleScope $ProjectName { - Describe class2 { - Context 'Type creation' { - It 'Has created a type named class2' { - 'class2' -as [Type] | Should -BeOfType [Type] - } - } - - Context 'Constructors' { - It 'Has a default constructor' { - $instance = [class2]::new() - $instance | Should -Not -BeNullOrEmpty - $instance.GetType().Name | Should -Be 'class2' - } - } - - Context 'Methods' { - BeforeEach { - $instance = [class2]::new() - } - - It 'Overrides the ToString method' { - # Typo "calss" is inherited from definition. Preserved here as validation is demonstrative. - $instance.ToString() | Should -Be 'This calss is class2' - } - } - - Context 'Properties' { - BeforeEach { - $instance = [class2]::new() - } - - It 'Has a Name property' { - $instance.Name | Should -Be 'Class2' - } - } - } -} diff --git a/tests/Unit/Private/Get-PrivateFunction.tests.ps1 b/tests/Unit/Private/Get-PrivateFunction.tests.ps1 deleted file mode 100644 index 138a377..0000000 --- a/tests/Unit/Private/Get-PrivateFunction.tests.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -BeforeAll { - $script:dscModuleName = 'WinProfileOps' - - Import-Module -Name $script:dscModuleName -} - -AfterAll { - # 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-PrivateFunction { - Context 'When calling the function with string value' { - It 'Should return a single object' { - InModuleScope -ModuleName $dscModuleName { - $return = Get-PrivateFunction -PrivateData 'string' - - ($return | Measure-Object).Count | Should -Be 1 - } - } - - It 'Should return a string based on the parameter PrivateData' { - InModuleScope -ModuleName $dscModuleName { - $return = Get-PrivateFunction -PrivateData 'string' - - $return | Should -Be 'string' - } - } - } -} - diff --git a/tests/Unit/Public/Get-Something.tests.ps1 b/tests/Unit/Public/Get-Something.tests.ps1 deleted file mode 100644 index 557624b..0000000 --- a/tests/Unit/Public/Get-Something.tests.ps1 +++ /dev/null @@ -1,91 +0,0 @@ -BeforeAll { - $script:dscModuleName = 'WinProfileOps' - - Import-Module -Name $script:dscModuleName -} - -AfterAll { - # 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-Something { - BeforeAll { - Mock -CommandName Get-PrivateFunction -MockWith { - # This return the value passed to the Get-PrivateFunction parameter $PrivateData. - $PrivateData - } -ModuleName $dscModuleName - } - - Context 'When passing values using named parameters' { - It 'Should call the private function once' { - { Get-Something -Data 'value' } | Should -Not -Throw - - Should -Invoke -CommandName Get-PrivateFunction -Exactly -Times 1 -Scope It -ModuleName $dscModuleName - } - - It 'Should return a single object' { - $return = Get-Something -Data 'value' - - ($return | Measure-Object).Count | Should -Be 1 - } - - It 'Should return the correct string value' { - $return = Get-Something -Data 'value' - - $return | Should -Be 'value' - } - } - - Context 'When passing values over the pipeline' { - It 'Should call the private function two times' { - { 'value1', 'value2' | Get-Something } | Should -Not -Throw - - Should -Invoke -CommandName Get-PrivateFunction -Exactly -Times 2 -Scope It -ModuleName $dscModuleName - } - - It 'Should return an array with two items' { - $return = 'value1', 'value2' | Get-Something - - $return.Count | Should -Be 2 - } - - It 'Should return an array with the correct string values' { - $return = 'value1', 'value2' | Get-Something - - $return[0] | Should -Be 'value1' - $return[1] | Should -Be 'value2' - } - - It 'Should accept values from the pipeline by property name' { - $return = 'value1', 'value2' | ForEach-Object { - [PSCustomObject]@{ - Data = $_ - OtherProperty = 'other' - } - } | Get-Something - - $return[0] | Should -Be 'value1' - $return[1] | Should -Be 'value2' - } - } - - Context 'When passing WhatIf' { - It 'Should support the parameter WhatIf' { - (Get-Command -Name 'Get-Something').Parameters.ContainsKey('WhatIf') | Should -Be $true - } - - It 'Should not call the private function' { - { Get-Something -Data 'value' -WhatIf } | Should -Not -Throw - - Should -Invoke -CommandName Get-PrivateFunction -Exactly -Times 0 -Scope It -ModuleName $dscModuleName - } - - It 'Should return $null' { - $return = Get-Something -Data 'value' -WhatIf - - $return | Should -BeNullOrEmpty - } - } -} -