DSCResources/DSC_UserAccountControl/DSC_UserAccountControl.psm1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Scope = 'Function')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'DSCMachineStatus', Justification = 'GlobalDsc Variable can be ignored')]
param ()

$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules'

# Import the ComputerManagementDsc Common Modules
Import-Module -Name (Join-Path -Path $modulePath `
        -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' `
            -ChildPath 'ComputerManagementDsc.Common.psm1'))

Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common')

# Import Localization Strings
$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'

$script:registryKey = 'HKLM:\HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System'

$script:granularUserAccountControlParameterNames = @(
    'FilterAdministratorToken'
    'ConsentPromptBehaviorAdmin'
    'ConsentPromptBehaviorUser'
    'EnableInstallerDetection'
    'ValidateAdminCodeSignatures'
    'EnableLua'
    'PromptOnSecureDesktop'
    'EnableVirtualization'
)

<#
    .SYNOPSIS
        Gets the current state of the User Account Control.
 
    .PARAMETER IsSingleInstance
        Specifies the resource is a single instance, the value must be 'Yes'.
 
    .PARAMETER SuppressRestart
        Specifies if a restart of the node should be suppressed. By default the
        node will be restarted if the value is changed.
#>

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        # This is best practice when writing a single-instance DSC resource.
        [Parameter(Mandatory = $true)]
        [ValidateSet('Yes')]
        [System.String]
        $IsSingleInstance,

        [Parameter()]
        [System.Boolean]
        $SuppressRestart = $false
    )

    Write-Verbose -Message $script:localizedData.GettingStateMessage

    $userAccountControlValues = Get-UserAccountControl

    $returnValue = @{
        IsSingleInstance            = 'Yes'
        NotificationLevel           = Get-NotificationLevel
        FilterAdministratorToken    = $userAccountControlValues.FilterAdministratorToken
        ConsentPromptBehaviorAdmin  = $userAccountControlValues.ConsentPromptBehaviorAdmin
        ConsentPromptBehaviorUser   = $userAccountControlValues.ConsentPromptBehaviorUser
        EnableInstallerDetection    = $userAccountControlValues.EnableInstallerDetection
        ValidateAdminCodeSignatures = $userAccountControlValues.ValidateAdminCodeSignatures
        EnableLua                   = $userAccountControlValues.EnableLua
        PromptOnSecureDesktop       = $userAccountControlValues.PromptOnSecureDesktop
        EnableVirtualization        = $userAccountControlValues.EnableVirtualization
        SuppressRestart             = $SuppressRestart
    }

    return $returnValue
}

<#
    .SYNOPSIS
        Sets the current state of the User Account Control.
 
    .PARAMETER IsSingleInstance
        Specifies the resource is a single instance, the value must be 'Yes'.
 
    .PARAMETER NotificationLevel
        Specifies the desired notification level for the User Account Control
        setting. This parameter can not be used at the same time as any of the
        granular parameters.
 
    .PARAMETER FilterAdministratorToken
        Specifies the mode for the built-in administrator account (RID 500).
 
    .PARAMETER ConsentPromptBehaviorAdmin
        Specifies the prompt behavior for the Consent Administrator.
 
    .PARAMETER ConsentPromptBehaviorUser
        Specifies how the operations that requires elevation is handled for users.
 
    .PARAMETER EnableInstallerDetection
        Specifies how package installations are handled.
 
    .PARAMETER ValidateAdminCodeSignatures
        Specifies how cryptographic signatures on interactive applications are
        handled.
 
    .PARAMETER EnableLua
        Specifies how the 'administrator in Admin Approval Mode' user type are
        handled.
 
    .PARAMETER PromptOnSecureDesktop
        Specifies if secure desktop prompting are used.
 
    .PARAMETER EnableVirtualization
        Specifies how redirection of legacy application File and Registry writes
        are handled.
 
    .PARAMETER SuppressRestart
        Specifies if a restart of the node should be suppressed. By default the
        node will be restarted if the value is changed.
#>

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Yes')]
        [System.String]
        $IsSingleInstance,

        [Parameter()]
        [ValidateSet('AlwaysNotify', 'AlwaysNotifyAndAskForCredentials', 'NotifyChanges', 'NotifyChangesWithoutDimming', 'NeverNotify', 'NeverNotifyAndDisableAll')]
        [System.String]
        $NotificationLevel,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $FilterAdministratorToken,

        [Parameter()]
        [ValidateSet(0, 1, 2, 3, 4, 5)]
        [System.UInt16]
        $ConsentPromptBehaviorAdmin,

        [Parameter()]
        [ValidateSet(0, 1, 3)]
        [System.UInt16]
        $ConsentPromptBehaviorUser,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $EnableInstallerDetection,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $ValidateAdminCodeSignatures,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $EnableLua,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $PromptOnSecureDesktop,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $EnableVirtualization,

        [Parameter()]
        [System.Boolean]
        $SuppressRestart = $false
    )

    $assertBoundParameterParameters = @{
        BoundParameterList     = $PSBoundParameters
        MutuallyExclusiveList1 = @(
            'NotificationLevel'
        )
        MutuallyExclusiveList2 = $script:granularUserAccountControlParameterNames
    }

    Assert-BoundParameter @assertBoundParameterParameters

    Write-Verbose -Message $script:localizedData.SettingStateMessage

    $needRestart = $false

    $getTargetResourceResult = Get-TargetResource -IsSingleInstance 'Yes' -SuppressRestart $SuppressRestart

    if ($PSBoundParameters.ContainsKey('NotificationLevel'))
    {
        if ($getTargetResourceResult.NotificationLevel -ne $NotificationLevel)
        {
            Write-Verbose -Message (
                $script:localizedData.SetNotificationLevel -f $NotificationLevel
            )

            Set-UserAccountControlToNotificationLevel -NotificationLevel $NotificationLevel

            $needRestart = $true
        }
        else
        {
            Write-Verbose -Message $script:localizedData.NotificationLevelInDesiredState
        }
    }
    else
    {
        foreach ($parameterName in $script:granularUserAccountControlParameterNames)
        {
            if ($PSBoundParameters.ContainsKey($parameterName) -and $getTargetResourceResult.$parameterName -ne $PSBoundParameters.$parameterName)
            {
                Write-Verbose -Message (
                    $script:localizedData.SetPropertyToValue `
                        -f $parameterName, $PSBoundParameters.$parameterName
                )

                try
                {
                    $setItemPropertyParameters = @{
                        Path        = $script:registryKey
                        Name        = $parameterName
                        Value       = $PSBoundParameters.$parameterName
                        Type        = 'DWord'
                        ErrorAction = 'Stop'
                    }

                    Set-ItemProperty @setItemPropertyParameters
                }
                catch
                {
                    New-InvalidOperationException `
                        -Message ($script:localizedData.FailedToSetGranularProperty -f $parameterName) `
                        -ErrorRecord $_
                }

                $needRestart = $true
            }
        }

        if (-not $needRestart)
        {
            Write-Verbose -Message $script:localizedData.GranularPropertiesInDesiredState
        }
    }

    if ($needRestart)
    {
        if ($SuppressRestart)
        {
            Write-Warning -Message $script:localizedData.SuppressRestart
        }
        else
        {
            $global:DSCMachineStatus = 1
        }
    }
}

<#
    .SYNOPSIS
        Tests the current state of the User Account Control.
 
    .PARAMETER IsSingleInstance
        Specifies the resource is a single instance, the value must be 'Yes'.
 
    .PARAMETER NotificationLevel
        Specifies the desired notification level for the User Account Control
        setting. This parameter can not be used at the same time as any of the
        granular parameters.
 
    .PARAMETER FilterAdministratorToken
        Specifies the mode for the built-in administrator account (RID 500).
 
    .PARAMETER ConsentPromptBehaviorAdmin
        Specifies the prompt behavior for the Consent Administrator.
 
    .PARAMETER ConsentPromptBehaviorUser
        Specifies how the operations that requires elevation is handled for users.
 
    .PARAMETER EnableInstallerDetection
        Specifies how package installations are handled.
 
    .PARAMETER ValidateAdminCodeSignatures
        Specifies how cryptographic signatures on interactive applications are
        handled.
 
    .PARAMETER EnableLua
        Specifies how the 'administrator in Admin Approval Mode' user type are
        handled.
 
    .PARAMETER PromptOnSecureDesktop
        Specifies if secure desktop prompting are used.
 
    .PARAMETER EnableVirtualization
        Specifies how redirection of legacy application File and Registry writes
        are handled.
 
    .PARAMETER SuppressRestart
        Specifies if a restart of the node should be suppressed. By default the
        node will be restarted if the value is changed.
#>

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Yes')]
        [System.String]
        $IsSingleInstance,

        [Parameter()]
        [ValidateSet('AlwaysNotify', 'AlwaysNotifyAndAskForCredentials', 'NotifyChanges', 'NotifyChangesWithoutDimming', 'NeverNotify', 'NeverNotifyAndDisableAll')]
        [System.String]
        $NotificationLevel,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $FilterAdministratorToken,

        [Parameter()]
        [ValidateSet(0, 1, 2, 3, 4, 5)]
        [System.UInt16]
        $ConsentPromptBehaviorAdmin,

        [Parameter()]
        [ValidateSet(0, 1, 3)]
        [System.UInt16]
        $ConsentPromptBehaviorUser,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $EnableInstallerDetection,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $ValidateAdminCodeSignatures,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $EnableLua,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $PromptOnSecureDesktop,

        [Parameter()]
        [ValidateSet(0, 1)]
        [System.UInt16]
        $EnableVirtualization,

        [Parameter()]
        [System.Boolean]
        $SuppressRestart = $false
    )

    Write-Verbose -Message $script:localizedData.TestingStateMessage

    $assertBoundParameterParameters = @{
        BoundParameterList     = $PSBoundParameters
        MutuallyExclusiveList1 = @(
            'NotificationLevel'
        )
        MutuallyExclusiveList2 = $script:granularUserAccountControlParameterNames
    }

    Assert-BoundParameter @assertBoundParameterParameters

    $getTargetResourceResult = Get-TargetResource -IsSingleInstance 'Yes' -SuppressRestart $SuppressRestart

    if ($PSBoundParameters.ContainsKey('NotificationLevel'))
    {
        if ($getTargetResourceResult.NotificationLevel -ne $NotificationLevel)
        {
            $testTargetResourceReturnValue = $false

            Write-Verbose -Message ($script:localizedData.NotificationLevelNoInDesiredState -f $getTargetResourceResult.NotificationLevel, $NotificationLevel)
        }
        else
        {
            $testTargetResourceReturnValue = $true

            Write-Verbose -Message $script:localizedData.NotificationLevelInDesiredState
        }
    }
    else
    {
        $testTargetResourceReturnValue = $true

        foreach ($parameterName in $script:granularUserAccountControlParameterNames)
        {
            if ($PSBoundParameters.ContainsKey($parameterName) -and $getTargetResourceResult.$parameterName -ne $PSBoundParameters.$parameterName)
            {
                $testTargetResourceReturnValue = $false

                Write-Verbose -Message ($script:localizedData.GranularPropertyNoInDesiredState -f $parameterName, $getTargetResourceResult.$parameterName, $PSBoundParameters.$parameterName)
            }
        }

        if ($testTargetResourceReturnValue)
        {
            Write-Verbose -Message $script:localizedData.GranularPropertiesInDesiredState
        }
    }

    return $testTargetResourceReturnValue
}

<#
    .SYNOPSIS
        Gets the current values of the User Account Control registry entries.
 
    .OUTPUTS
        Returns a hashtable containing the values.
#>

function Get-UserAccountControl
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param ()

    return @{
        FilterAdministratorToken    = Get-RegistryPropertyValue -Path $script:registryKey -Name 'FilterAdministratorToken'
        ConsentPromptBehaviorAdmin  = Get-RegistryPropertyValue -Path $script:registryKey -Name 'ConsentPromptBehaviorAdmin'
        ConsentPromptBehaviorUser   = Get-RegistryPropertyValue -Path $script:registryKey -Name 'ConsentPromptBehaviorUser'
        EnableInstallerDetection    = Get-RegistryPropertyValue -Path $script:registryKey -Name 'EnableInstallerDetection'
        ValidateAdminCodeSignatures = Get-RegistryPropertyValue -Path $script:registryKey -Name 'ValidateAdminCodeSignatures'
        EnableLua                   = Get-RegistryPropertyValue -Path $script:registryKey -Name 'EnableLUA'
        PromptOnSecureDesktop       = Get-RegistryPropertyValue -Path $script:registryKey -Name 'PromptOnSecureDesktop'
        EnableVirtualization        = Get-RegistryPropertyValue -Path $script:registryKey -Name 'EnableVirtualization'
    }
}

<#
    .SYNOPSIS
        Gets the current notification level string value.
 
    .OUTPUTS
        Returns the notification level string value. If the registry values does
        not match a predefined notification level then $null is returned.
#>

function Get-NotificationLevel
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param ()

    $notificationLevelStringValue = $null

    $userAccountControlValues = Get-UserAccountControl

    if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 2 `
            -and $userAccountControlValues.EnableLua -eq 1 `
            -and $userAccountControlValues.PromptOnSecureDesktop -eq 1
    )
    {
        $notificationLevelStringValue = 'AlwaysNotify'
    }

    if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 1 `
            -and $userAccountControlValues.EnableLua -eq 1 `
            -and $userAccountControlValues.PromptOnSecureDesktop -eq 1
    )
    {
        $notificationLevelStringValue = 'AlwaysNotifyAndAskForCredentials'
    }

    if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 5 `
            -and $userAccountControlValues.EnableLua -eq 1 `
            -and $userAccountControlValues.PromptOnSecureDesktop -eq 1
    )
    {
        $notificationLevelStringValue = 'NotifyChanges'
    }

    if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 5 `
            -and $userAccountControlValues.EnableLua -eq 1 `
            -and $userAccountControlValues.PromptOnSecureDesktop -eq 0
    )
    {
        $notificationLevelStringValue = 'NotifyChangesWithoutDimming'
    }

    if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 0 `
            -and $userAccountControlValues.EnableLua -eq 1 `
            -and $userAccountControlValues.PromptOnSecureDesktop -eq 0
    )
    {
        $notificationLevelStringValue = 'NeverNotify'
    }

    if ($userAccountControlValues.ConsentPromptBehaviorAdmin -eq 0 `
            -and $userAccountControlValues.EnableLua -eq 0 `
            -and $userAccountControlValues.PromptOnSecureDesktop -eq 0
    )
    {
        $notificationLevelStringValue = 'NeverNotifyAndDisableAll'
    }

    return $notificationLevelStringValue
}

<#
    .SYNOPSIS
        Gets the current notification level string value.
 
    .OUTPUTS
        Returns the notification level string value. If the registry values does
        not match a predefined notification level then $null is returned.
#>

function Set-UserAccountControlToNotificationLevel
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('AlwaysNotify', 'AlwaysNotifyAndAskForCredentials', 'NotifyChanges', 'NotifyChangesWithoutDimming', 'NeverNotify', 'NeverNotifyAndDisableAll')]
        [System.String]
        $NotificationLevel
    )

    try
    {
        $defaultSetItemPropertyParameters = @{
            Path        = $script:registryKey
            Type        = 'DWord'
            ErrorAction = 'Stop'
        }

        switch ($NotificationLevel)
        {
            'AlwaysNotify'
            {
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 2
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 1
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 1
            }

            'AlwaysNotifyAndAskForCredentials'
            {
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 1
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 1
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 1
            }


            'NotifyChanges'
            {
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 5
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 1
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 1
            }

            'NotifyChangesWithoutDimming'
            {
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 5
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 1
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 0
            }

            'NeverNotify'
            {
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 0
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 1
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 0
            }

            'NeverNotifyAndDisableAll'
            {
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'ConsentPromptBehaviorAdmin' -Value 0
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'EnableLUA' -Value 0
                Set-ItemProperty @defaultSetItemPropertyParameters -Name 'PromptOnSecureDesktop' -Value 0
            }
        }
    }
    catch
    {
        New-InvalidOperationException `
            -Message ($script:localizedData.FailedToSetNotificationLevel -f $NotificationLevel) `
            -ErrorRecord $_
    }
}