Skip to content

Commit

Permalink
Merge pull request #5 from LarryWisherMan/feature/FunctionEnhancements
Browse files Browse the repository at this point in the history
Feature/function enhancements
  • Loading branch information
LarryWisherMan authored Sep 12, 2024
2 parents ae80043 + 648defd commit dd185f4
Show file tree
Hide file tree
Showing 10 changed files with 857 additions and 53 deletions.
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- 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.

- **Get-SIDProfileInfoFallback**: Introduced a new fallback function
`Get-SIDProfileInfoFallback` that retrieves non-special user profile
information using the CIM/WMI method.

### 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.

## [0.2.0] - 2024-09-12

### Added
Expand Down
3 changes: 1 addition & 2 deletions build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ Encoding: UTF8
#SemVer: '99.0.0-preview1'

# Suffix to add to Root module PSM1 after merge (here, the Set-Alias exporting IB tasks)
# suffix: suffix.ps1
# prefix: prefix.ps1
prefix: prefix.ps1
VersionedOutputDirectory: true

####################################################
Expand Down
5 changes: 3 additions & 2 deletions source/Private/Get-SIDProfileInfo.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,8 @@ function Get-SIDProfileInfo
$ProfileRegistryItems = foreach ($sid in $subKeyNames)
{
# Validate SID format (SIDs typically start with 'S-1-' and follow a specific pattern)
if ($sid -notmatch '^S-1-\d+-\d+(-\d+){1,}$')
if (-not (Validate-SIDFormat -SID $sid))
{
Write-Warning "Invalid SID format encountered: '$sid'. Skipping."
continue
}

Expand Down Expand Up @@ -99,3 +98,5 @@ function Get-SIDProfileInfo

return $ProfileRegistryItems
}


59 changes: 59 additions & 0 deletions source/Private/Get-SIDProfileInfoFallback.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<#
.SYNOPSIS
Retrieves non-special user profile information from a remote or local computer.
.DESCRIPTION
The Get-SIDProfileInfoFallback function uses the CIM (Common Information Model) method to retrieve user profile
information from the specified computer. It filters out special profiles and returns the SID, profile path, and other
relevant information for each user profile found on the system. This function serves as a fallback method for obtaining
profile information without requiring administrative privileges to access the registry.
.PARAMETER ComputerName
The name of the computer to query for user profiles. If not provided, the function will default to the local computer.
.OUTPUTS
[PSCustomObject[]]
Returns an array of PSCustomObject where each object contains:
- SID: The security identifier for the user profile.
- ProfilePath: The local file system path to the user profile.
- ComputerName: The name of the computer from which the profile was retrieved.
- ExistsInRegistry: Always set to $true, as this function is a fallback and does not query the registry directly.
.EXAMPLE
Get-SIDProfileInfoFallback
Retrieves non-special user profiles from the local computer and returns their SID, profile path, and other details.
.EXAMPLE
Get-SIDProfileInfoFallback -ComputerName "Server01"
Retrieves non-special user profiles from the remote computer "Server01" and returns their SID, profile path, and other details.
.NOTES
This function does not require administrative privileges to access profile information, as it relies on CIM/WMI methods
to retrieve data. It specifically filters out special profiles (such as system profiles) using the "Special=False" filter.
#>

function Get-SIDProfileInfoFallback
{
[OutputType([PSCustomObject[]])]
[CmdletBinding()]
param (
[string]$ComputerName = $env:COMPUTERNAME
)
# Use CIM as a fallback method to get user profile information
$profiles = Get-CimInstance -ClassName Win32_UserProfile -ComputerName $ComputerName -Filter "Special=False"

$ProfileRegistryItems = foreach ($profile in $profiles)
{
# Return a PSCustomObject similar to what Get-SIDProfileInfo returns
[PSCustomObject]@{
SID = $profile.SID
ProfilePath = $profile.LocalPath
ComputerName = $ComputerName
ExistsInRegistry = $true
}
}

return $ProfileRegistryItems
}
57 changes: 57 additions & 0 deletions source/Private/Validate-SIDFormat.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<#
.SYNOPSIS
Validates whether a given string follows the correct SID (Security Identifier) format.
.DESCRIPTION
The Validate-SIDFormat function checks if a given string matches the standard SID format.
SIDs typically start with 'S-1-' followed by a series of digits separated by hyphens.
This function returns $true if the SID format is valid and $false if it is not.
.PARAMETER SID
The SID string to validate. This should follow the typical format: 'S-1-' followed by
a series of digits and hyphens.
.OUTPUTS
[bool]
Returns $true if the SID format is valid; otherwise, returns $false.
.EXAMPLE
PS> Validate-SIDFormat -SID 'S-1-5-18'
True
This example checks if the SID 'S-1-5-18' is valid.
.EXAMPLE
PS> Validate-SIDFormat -SID 'Invalid-SID'
WARNING: Invalid SID format encountered: 'Invalid-SID'.
False
This example demonstrates how the function handles an invalid SID format by returning $false
and issuing a warning.
.NOTES
.LINK
https://docs.microsoft.com/en-us/windows/win32/secauthz/security-identifiers
#>
function Validate-SIDFormat
{
param (
[OutPutType([bool])]
[CmdletBinding()]
[Parameter(Mandatory = $true)]
[string]$SID
)

# Regular expression pattern for validating the SID format
$sidPattern = '^S-1-\d+(-\d+)+$'

if ($SID -notmatch $sidPattern)
{
Write-Warning "Invalid SID format encountered: '$SID'."
return $false
}

return $true
}
15 changes: 12 additions & 3 deletions source/Public/Get-UserProfilesFromRegistry.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,18 @@ function Get-UserProfilesFromRegistry
return @() # Return an empty array
}

# Get registry profiles and return them
$RegistryProfiles = Get-SIDProfileInfo -ComputerName $ComputerName -ErrorAction Stop
return $RegistryProfiles
# If user is an admin, use Get-SIDProfileInfo (Registry-based)
if ($ENV:WinProfileOps_IsAdmin -eq $true)
{
Write-Verbose "User has administrator privileges, using registry-based method."
return Get-SIDProfileInfo -ComputerName $ComputerName
}
else
{
Write-Warning "User lacks administrator privileges. Switching to fallback method which excludes special accounts from the results."
return Get-SIDProfileInfoFallback -ComputerName $ComputerName
}

}
catch
{
Expand Down
36 changes: 36 additions & 0 deletions source/prefix.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 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)

# 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
Write-Verbose "WinProfileOps: Removed WinProfileOps_IsAdmin environment variable."
}
}

Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
$sb.Invoke()
}

# Define the OnRemove script block for the module
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
$sb.Invoke()
}
144 changes: 144 additions & 0 deletions tests/Unit/Private/Get-SIDProfileInfoFallback.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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-SIDProfileInfoFallback' -Tag 'Private' {

Context 'When retrieving user profiles via fallback' {

BeforeEach {
InModuleScope -ScriptBlock {
# Mock Get-CimInstance to simulate returning non-special profiles
Mock Get-CimInstance {
return @(
[PSCustomObject]@{ SID = 'S-1-5-21-1001'; LocalPath = 'C:\Users\User1' },
[PSCustomObject]@{ SID = 'S-1-5-21-1002'; LocalPath = 'C:\Users\User2' }
)
}
}
}

It 'Should return profiles from the local computer' {
$ComputerName = $env:COMPUTERNAME

# Call the private function within the module scope
$result = InModuleScope -ScriptBlock {
Get-SIDProfileInfoFallback -ComputerName $ComputerName
}

# Validate the results
$result | Should -HaveCount 2
$result[0].SID | Should -Be 'S-1-5-21-1001'
$result[0].ProfilePath | Should -Be 'C:\Users\User1'
$result[1].SID | Should -Be 'S-1-5-21-1002'
$result[1].ProfilePath | Should -Be 'C:\Users\User2'

# Assert that Get-CimInstance was called once
Assert-MockCalled Get-CimInstance -Exactly 1
}

It 'Should return profiles from a remote computer' {
$result = InModuleScope -ScriptBlock {

$ComputerName = 'RemotePC'

# Call the private function within the module scope

Get-SIDProfileInfoFallback -ComputerName $ComputerName
}

# Validate the results
$result | Should -HaveCount 2
$result[0].SID | Should -Be 'S-1-5-21-1001'
$result[0].ProfilePath | Should -Be 'C:\Users\User1'
$result[1].SID | Should -Be 'S-1-5-21-1002'
$result[1].ProfilePath | Should -Be 'C:\Users\User2'
$result[0].ComputerName | Should -Be 'RemotePC'
$result[1].ComputerName | Should -Be 'RemotePC'

# Assert that Get-CimInstance was called once
Assert-MockCalled Get-CimInstance -Exactly 1
}
}

Context 'When no profiles are returned' {
BeforeEach {
InModuleScope -ScriptBlock {
# Mock Get-CimInstance to return an empty list
Mock Get-CimInstance {
return @()
}
}
}

It 'Should return an empty result when no profiles are found' {
$ComputerName = $env:COMPUTERNAME

# Call the private function within the module scope
$result = InModuleScope -ScriptBlock {
Get-SIDProfileInfoFallback -ComputerName $ComputerName
}

# Validate the result is empty
$result | Should -BeNullOrEmpty

# Assert that Get-CimInstance was called once
Assert-MockCalled Get-CimInstance -Exactly 1
}
}

Context 'When Get-CimInstance throws an error' {
BeforeEach {
InModuleScope -ScriptBlock {
# Mock Get-CimInstance to throw an error
Mock Get-CimInstance {
throw "CIM query failed"
}

# Mock Write-Error to capture the error
Mock Write-Error
}
}

It 'Should log an error and return nothing when Get-CimInstance fails' {
$ComputerName = $env:COMPUTERNAME

# Call the private function within the module scope
$result = InModuleScope -ScriptBlock {
try
{
Get-SIDProfileInfoFallback -ComputerName $ComputerName
}
catch
{
Write-Error "Error retrieving profiles via CIM"
}
}

# The result should be empty
$result | Should -BeNullOrEmpty

# Assert that Write-Error was called
Assert-MockCalled Write-Error -Exactly 1

# Assert that Get-CimInstance was called once
Assert-MockCalled Get-CimInstance -Exactly 1
}
}

}
Loading

0 comments on commit dd185f4

Please sign in to comment.