DSCResources/MSFT_xJeaEndpoint/MSFT_xJeaEndpoint.psm1

#requires -version 5
#region Helper
# Internal function to throw terminating error with specified errorCategory, errorId and errorMessage
function New-TerminatingError
{
    param
    (
        [Parameter(Mandatory)]
        [String]$errorId,
        
        [Parameter(Mandatory)]
        [String]$errorMessage,

        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorCategory]$errorCategory
    )
    
    $exception   = New-Object System.InvalidOperationException $errorMessage 
    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $null
    throw $errorRecord
}


function Get-JeaDir
{
    Join-Path $env:ProgramFiles 'Jea'
}

function Get-JeaToolKitDir
{
    Join-Path (Get-JeaDir) 'Toolkit'
}

function Get-JeaUtilDir
{
    Join-Path (Get-JeaDir) 'Util'
}

function Get-JeaStartupScriptDir
{
    Join-Path (Get-JeaDir) 'StartupScript'
}

function Get-JeaActivityDir
{
    Join-Path (Get-JeaDir) 'Activity'
}


<#
.Synopsis
   Creates a random password.
.DESCRIPTION
   Creates a random password by generating a array of characters and passing it to Get-Random
.EXAMPLE
PS> New-RandomPassword
g0dIDojsRGcV
.EXAMPLE
PS> New-RandomPassword -Length 3
dyN
.EXAMPLE
PS> New-RandomPassword -Length 30 -UseSpecialCharacters
r5Lhs1K9n*joZl$u^NDO&TkWvPCf2c
#>

function New-RandomPassword
{
    [CmdletBinding()]
    [OutputType([String])]
    Param
    (
        # Length of the password
        [Parameter(Mandatory=$False, Position=0)]
        [ValidateRange(12, 127)]
        $Length=12,

        # Includes the characters !@#$%^&*-+ in the password
        [switch]$UseSpecialCharacters
    )

    [char[]]$allowedCharacters = ([Char]'a'..[char]'z') + ([char]'A'..[char]'Z') + ([byte][char]'0'..[byte][char]'9')
    if ($UseSpecialCharacters)
    {
        foreach ($c in '!','@','#','$','%','^','&','*','-','+')
        {
            $allowedCharacters += [char]$c
        }
    }

    $characters = 1..$Length | % {
        $characterIndex = Get-Random -Minimum 0 -Maximum $allowedCharacters.Count
        $allowedCharacters[$characterIndex]
    }

    return (-join $characters)
}

Set-Alias New-RandomString New-RandomPassword

<#
.Synopsis
   Create a User Account with a random name
.DESCRIPTION
    
.EXAMPLE
   Example of how to use this cmdlet
.EXAMPLE
   Another example of how to use this cmdlet
#>

function New-JeaSessionAccount
{
    [CmdletBinding(DefaultParameterSetName='UserNamePrefix')]
    Param
    (
        #********************************************************************************
        # A random username is generated. This parameter gives you the opportunity to
        # to prefix that name with a string to help identify the account
        [Parameter(Mandatory=$false, ParameterSetName='UserNamePrefix')]
        [ValidateLength(0,12)]
        $UserNamePrefix = 'JSA-',

        [Parameter(Mandatory=$true, ParameterSetName='UserName')]
        $UserName,

        [string]
        $ComputerName = $Env:COMPUTERNAME,

        [string]
        $Description = "JEA Service Account created by $($env:UserDomain + '\' + $ENV:Username) at $(Get-Date)",

        [string[]]
        $Group='Administrators'
    )

    if (!($PSCmdlet.ParameterSetName -eq 'UserName'))
    {
        $MaxWindowsUserLength = 20
        $Templength = $MaxWindowsUserLength - $UserNamePrefix.Length
        $UserName = $UserNamePrefix + (New-RandomString -Length $TempLength)
    }

    $Computer = [ADSI]"WinNT://$COMPUTERNAME,Computer"

    Write-Verbose "New [JeaSessionAccount]$UserName"
    $User = $Computer.Create('User', $UserName)
    $password = New-RandomPassword -Length 127 -UseSpecialCharacters
    $cred = new-object pscredential $username, (convertto-securestring $password -asplaintext -force)

    $null = $(
      $User.SetPassword($password)
      $User.SetInfo()
      $User.FullName = 'Jea Session Account'
      $User.SetInfo()
      $User.Description = $Description
      $User.SetInfo()
      $User.UserFlags = 64 # ADS_UF_PASSWD_CANT_CHANGE http://msdn.microsoft.com/en-us/library/aa772300(v=vs.85).aspx
      $User.SetInfo()
    )
    
    foreach ($item in $group)
    {
        Write-Verbose " [JeaSessionAccount]$UserName ADD [Group]$item"
        $gobj = [ADSI]"WinNT://$COMPUTERNAME/$item,group" 
        $null = $gobj.add("WinNT://$ComputerName/$UserName") 
    }
    return $cred  
}

function Remove-JeaAccount
{
    Param
    (

        [Parameter(Mandatory=$true)]
        $UserName,

        [string]
        $ComputerName = $Env:COMPUTERNAME
    )

    $Computer = [ADSI]"WinNT://$COMPUTERNAME,Computer"
    $computer.delete("user",$userName)
}


function Test-JeaSessionAccount
{
    [CmdletBinding()]
    [OutputType([Boolean])]
        Param
    (
        [Parameter(Mandatory=$true, Position=0)]
        $UserName,

        $ComputerName = $Env:COMPUTERNAME

    )

    $oldDebugPrefernce = $DebugPreference; $oldVerbosePreference = $VerbosePreference
    $DebugPreference = $VerbosePreference = 'SilentlyContinue'
    $user = Get-CimInstance -Query "select * from Win32_UserAccount Where Name=""$UserName"" AND LocalAccount=""TRUE""" -ComputerName $ComputerName -Verbose:0
    $DebugPreference = $oldDebugPrefernce; $VerbosePreference = $oldVerbosePreference
    return [Boolean]$user
}


function Reset-JeaSessionAccountPassword
{
    [CmdletBinding()]
    Param
    (
        #********************************************************************************
        # A random username is generated. This parameter gives you the opportunity to
        # to prefix that name with a string to help identify the account
        [Parameter(Mandatory=$true, Position=0)]
        $UserName,

        $ComputerName = $Env:COMPUTERNAME

    )

    $user = [ADSI]"WinNT://$computerName/$username,user" 
    $password = New-RandomPassword -Length 127 -UseSpecialCharacters
    $null = $user.setpassword($password)  
    $null = $user.SetInfo() 
    $cred = new-object pscredential $username, (convertto-securestring $password -asplaintext -force)
    return $cred
}

function New-InitializationFile 
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([int])]
    Param
    (
        [Parameter(Mandatory=$true,
                   Position=0)]
        $Path,

        [Parameter(Position=1)]
        $Toolkit
    )
Write-Verbose "New [InitializationFile]$path"
""> $path
$toolkitNames = @()
foreach ($t in $toolkit)
{
    $toolkitNames += ("$t" +'-Toolkit')
}

@"
<#
This is a auto-generated JEA startup file
Generated At: $(Get-date)
Generated On: $(hostname)
Generated By: $($env:UserDomain + '\' + $env:UserName)
#>
function whoami {`$PSSenderInfo}
 
`$Init = (Join-Path `$env:ProgramFiles 'Jea\Util\Initialize-Toolkit.ps1')
. `$Init -toolkit $($Toolkitnames -join ',')
 
"@
 > $path
    
}
$ResetScript = @'
    #This resets the passwords for all the RUnas
    [char[]]$pwChars = ([Char]'a'..[char]'z') + ([char]'A'..[char]'Z') + ([byte][char]'0'..[byte][char]'9')
    foreach ($c in '!','@','#','$','%','^','&','*','-','+')
    {
        $pwChars += [char]$c
    }
 
    $endpoint = Get-PSSessionConfiguration |where {$_.RunAsUser -like 'JSA-*'}
    if ($endpoint)
    {
        foreach ($ep in $endpoint)
        {
            $user = [ADSI]"WinNT://$($env:computerName)/$($ep.RunAsUser),user"
            $password = (( $pwChars |Get-Random -Count 127 | % {[char]$_}) -join '')
            $null = $user.setpassword($password)
            $null = $user.SetInfo()
            $cred = new-object pscredential ($ep.RunAsUser), (convertto-securestring $password -asplaintext -force)
            Set-PSSessionConfiguration -Name $($ep.Name) -RunAsCredential $cred -Force
        }
        Restart-Service Winrm -Force
    }
'@


$RestartWinRM = @'
    "WinRM Restart start: $((get-date).GetDateTimeFormats()[112])" >> 'c:\program files\Jea\Activity\WinRMRestart.txt'
    While ((Get-DscLocalConfigurationManager).LocalConfigurationManagerState -ne 'Ready')
    {
        Start-Sleep -Seconds 5
    }
    Restart-Service winrm -Force
    "WinRM Restarted at : $((get-date).GetDateTimeFormats()[112])" >> 'c:\program files\Jea\Activity\WinRMRestart.txt'
 
'@


function Assert-ScheduledScripts
{
    $UtilDir = Get-JeaUtilDir
    if (!(Test-Path $UtilDir))
    {
        Write-Verbose "New [JeaDirectory]$utildir"
        mkdir -Force -Path $UtilDir
    }

    $ResetPS1 = Join-Path $UtilDir 'ScheduledPasswordReset.ps1'
    if (!(Test-Path $ResetPs1) -or 
        (cat $resetPS1 -Delimiter "None") -ne $resetscript)
    {
        Write-Verbose 'Reset [JeaScheduledPasswordResetScript]'
        $resetScript > $ResetPS1
    }
    
    $RestartWinRMPS1 = Join-Path $UtilDir 'RestartWinrm.ps1'
    if (!(Test-Path $restartWinRMPS1) -or 
        (Get-Content $restartWinRMPS1 -Delimiter 'None') -ne $RestartWinRM)
    {
        Write-Verbose 'Reset [JeaScheduledPasswordreStartWinRMScript]'
        $RestartWinRM > $restartWinRMPS1
    }
}

function Assert-ScheduledTasks
{
param(
[Parameter()]
$cred
)

    $UtilDir = Get-JeaUtilDir
    $wd = "`"$UtilDir`""
    $ResetPS1 = Join-Path $UtilDir 'ScheduledPasswordReset.ps1'
    $file = "`"$ResetPS1`""
    $Arguments = "-nologo -executionPolicy ByPass -NoProfile -NonInteractive -file $file -WorkingDirectory $wd -WindowStyle hidden" 
    $s = Get-ScheduledTask -TaskPath \Microsoft\Windows\Jea\ -TaskName ResetJeaSessionAccountPasswords -ErrorAction SilentlyContinue
    Write-Verbose "Test [JEAScheduledTask]ResetJeaSessionAccountPasswords $([Bool]$s)"
    if (!($s) -or
        $s.Actions.Count -ne 1  -or
        $s.Actions[0].Execute   -ne 'PowerShell.exe' -or
        $s.Actions[0].Arguments -ne $Arguments)
    {
        if ($s) {Unregister-ScheduledTask -TaskPath \Microsoft\Windows\Jea\ -TaskName ResetJeaSessionAccountPasswords -ErrorAction SilentlyContinue }
        $action = New-ScheduledTaskAction -Execute PowerShell.exe -Argument $arguments
        $trigger = New-ScheduledTaskTrigger -Daily -At 1:00
        if (!$Cred)
        {
            Write-Verbose 'Register [JEAScheduledTask]ResetJeaSessionAccountPasswords'
            $cred = Reset-JeaSessionAccountPassword -UserName JeaSchTaskAccount
        }
        $setting = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -StartWhenAvailable    
        Write-Verbose 'Register [JEAScheduledTask]ResetJeaSessionAccountPasswords'
        Register-ScheduledTask -TaskName ResetJeaSessionAccountPasswords -Action $action `
            -TaskPath \Microsoft\Windows\Jea\  -Description 'Reset Jea Session Account Passwords' `
            -Settings $setting `
            -Trigger $trigger `
            -User $cred.UserName -Password $cred.GetNetworkCredential().Password
        
    }


    $RestartWinRMPS1 = Join-Path $UtilDir 'RestartWinrm.ps1'
    $file = "`"$RestartWinrmPS1`""
    $Arguments = "-nologo -executionPolicy ByPass -NoProfile -NonInteractive -file $file -WorkingDirectory $wd -WindowStyle hidden" 
    $s = Get-ScheduledTask -TaskPath \Microsoft\Windows\Jea\ -TaskName RestartWinRM -ErrorAction SilentlyContinue
    Write-Verbose "Test [JEAScheduledTask]RestartWinRM $([Bool]$s)"
    if (!($s) -or
        $s.Actions.Count -ne 1  -or
        $s.Actions[0].Execute   -ne 'PowerShell.exe' -or
        $s.Actions[0].Arguments -ne $Arguments)
    {
        if ($s) {Unregister-ScheduledTask -TaskPath \Microsoft\Windows\Jea\ -TaskName RestartWinRM -ErrorAction SilentlyContinue }
        $action = New-ScheduledTaskAction -Execute PowerShell.exe -Argument $arguments
        if (!$Cred)
        {
            Write-Verbose 'Register [JEAScheduledTask]JeaSchTaskAccount'
            $cred = Reset-JeaSessionAccountPassword -UserName JeaSchTaskAccount
        }
        $settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew            
        Write-Verbose 'Register [JEAScheduledTask]RestartWinRM'
        Register-ScheduledTask -TaskName RestartWinRM -Action $action `
            -TaskPath \Microsoft\Windows\Jea\  -Description 'Restart WinRM after current DSC cycle is complete' `
            -Settings $settings `
            -User $cred.UserName -Password $cred.GetNetworkCredential().Password
        
    }
}
function Assert-JeaAdmin
{
    try
    {
        $cred = $null
        if (!(Test-JeaSessionAccount -UserName JeaSchTaskAccount))
        {
            Write-Verbose 'New [JeaScheduledTaskAccount]'
            $cred = New-JeaSessionAccount -UserName JeaSchTaskAccount -Group Administrators -Description "This is a special Jea account to run the ResetJeaSessionAccountPasswords Scheduled task"
        }
        Assert-ScheduledScripts
        Assert-ScheduledTasks $cred
    }catch
    {
        write-verbose "ERROR: $($_|fl * -force|out-string)"
        throw
    }
}
#endregion

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [parameter(Mandatory = $true)]
        [System.String]
        $Name
    )

    try
    {
        $endpoint = Get-PSSessionConfiguration -Name $Name -ErrorAction SilentlyContinue -Verbose:0
    
        if ($endpoint)
        {
            $returnValue = @{
                Name = [System.String]$Name
                Toolkit = [System.String[]]"TODO: GET TOOLKITS"
                Ensure = [System.String]'Present'
                SecurityDescriptorSddl = [System.String]$endpoint.SecurityDescriptorSddl
                Group = [String[]]$(
                    "TODO: Get Groups"
                )
            }
        }
        else
        {
            $returnValue = @{
                Name = [System.String]$Name
                Ensure = [System.String]'Absent'
            }
        }
        $returnValue
    }catch
    {
        write-Debug "ERROR: $($_|fl * -force|out-string)"
        New-TerminatingError -errorId 'SetJeaEndpointFailed' -errorMessage $_.Exception -errorCategory InvalidOperation

    }
    
}


function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [System.String[]]
        $Toolkit,

        [System.String]
        $SecurityDescriptorSddl='O:NSG:BAD:P(A;;GA;;;BA)(A;;GA;;;RM)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)',

        [System.String[]]
        $Group = @('Administrators'),

        [System.Boolean]
        $CleanAll,

        [ValidateSet('Present','Absent')]
        [System.String]
        $Ensure = 'Present'
    )
    try
    {    
        write-Verbose "$((get-date).GetDateTimeFormats()[112]) Start Setting [EndPoint]$Name" 
        if ($CleanAll)
        {
            #region Remove non-default endpoints
            Write-Verbose 'Remove [JeaEndpoints]*'
            $defaultEndpoints = 'microsoft.powershell','microsoft.powershell.workflow','microsoft.powershell32','microsoft.windows.servermanagerworkflows'
            Get-PSSessionConfiguration -Verbose:0 |where Name -NotIn $defaultEndpoints | % {
                Write-Verbose "Remove [PSEndpoint]$($_.Name)"
                Unregister-PSSessionConfiguration -NoServiceRestart -Force -Verbose:0 -Name $_.Name
            }
            #endregion
            
            #region Remove the JSA UserAccounts
            Write-Verbose 'Remove [JSAUserAccount]*'
            [ADSI]$Server="WinNT://$($Env:COMPUTERNAME)"
            foreach ($account in (Get-CimInstance -Query 'select * from Win32_UserAccount Where Name Like "JSA-%" AND LocalAccount="TRUE"').Name)
            {
                Write-Verbose "Remove [JSAUserAccount]$account"
                $server.Delete('user',$account)
            }
            if (Get-CimInstance -Query 'select * from Win32_UserAccount Where Name = "JeaSchTaskAccount" AND LocalAccount="TRUE"')
            {
                Write-Verbose "Remove [JSAUserAccount]JeaSchTaskAccount"
                $server.Delete('user','JeaSchTaskAccount')
            }
            #endregion

            #region remove StartupFiles
            if (test-path (Get-JeaStartupScriptDir))
            {
                Write-Verbose 'Remove [JeaStartupfile]*'
                Remove-Item (Join-Path (Get-JeaStartupScriptDir) *) -Recurse -Verbose
            }
            #endregion

            #region Remove JeaScheduled Tasks
            write-verbose "Remove [ScheduleTask]RestartWinRM"
            Unregister-ScheduledTask  -TaskPath \Microsoft\Windows\Jea\ -TaskName RestartWinRM -ErrorAction Ignore
            write-verbose "Remove [ScheduleTask]ResetJeaSessionAccountPasswords"
            Unregister-ScheduledTask  -TaskPath \Microsoft\Windows\Jea\ -TaskName ResetJeaSessionAccountPasswords -ErrorAction Ignore
            #endregion

        }
        else
        {
            Assert-JeaAdmin

            $endPointName = $Name
            $endPoint     = Get-PSSessionConfiguration -Name $Name -Verbose:0 -ErrorAction SilentlyContinue
            $account      = "JSA-$Name"
            if ($Ensure -AND !($endPoint))
            {
                #region ACCOUNT
                Write-Verbose "Test [JeaSessionAccount]$account"
                if (Test-JeaSessionAccount -UserName $account)
                {
                    Write-Verbose " [JeaSessionAccount]$account = `$true; Reset-JeaSessionAccountPassword"
                    $cred = Reset-JeaSessionAccountPassword -UserName $account
                }else
                {
                    Write-Verbose "New [JeaSessionAccount]$account"
                    $cred = New-JeaSessionAccount -UserName $account -Description 'PowerShell Session Acount' -Group $group
                }
                #endregion

                #region InitializationFile
                $startupfile = Join-Path (Get-JeaStartupScriptDir) "Initialize-$($Name).ps1"
                Write-Verbose "New [JeaStartupFile]$StartupFile -> [Jeatoolkit]$Toolkit"
                New-InitializationFile -Path $StartupFile -Toolkit $Toolkit
                #endregion

                #region EndPoint
                Write-Verbose "New [JeaEndPoint]$Name"
                try
                {
                    # Set the following preference so the functions inside Unregister-PSSessionConfig doesn't get these settings
                    $oldDebugPrefernce = $DebugPreference; $oldVerbosePreference = $VerbosePreference
                    $DebugPreference = $VerbosePreference = 'SilentlyContinue'

                    $null = Register-PSSessionConfiguration -Name $Name -StartupScript $startupfile -RunAsCredential $cred -NoServiceRestart -Verbose:0 -Force -SecurityDescriptorSddl $SecurityDescriptorSddl
                    # Reset the following preference to older values
                    $DebugPreference = $oldDebugPrefernce; $VerbosePreference = $oldVerbosePreference

                    Start-ScheduledTask -TaskName RestartWinRM -TaskPath \Microsoft\Windows\Jea\
                }
                catch
                {
                    write-Debug "ERROR: $($_|fl * -force|out-string)"
                    New-TerminatingError -errorId 'JeaEndpointConfigurationFailed' -errorMessage $_.Exception -errorCategory InvalidOperation
                }            

                #endregion
            }elseif (!($Ensure) -And $EndPoint)
            {
                Write-Verbose "Unregister [JeaEndpoint]$Name"
                Unregister-PSSessionConfiguration -Name $Name -Force -Verbose:0
                Start-ScheduledTask -TaskName RestartWinRM -TaskPath \Microsoft\Windows\Jea\
                if (Test-JeaSessionAccount -UserName $account)
                {
                    [ADSI]$Server="WinNT://$($Env:COMPUTERNAME)"
                    $server.Delete('user',$account)
                }
            }
        }
    }catch
    {
        write-Debug "ERROR: $($_|fl * -force|out-string)"
        New-TerminatingError -errorId 'SetJeaEndpointFailed' -errorMessage $_.Exception -errorCategory InvalidOperation

    }finally
    {
        write-Verbose "$((get-date).GetDateTimeFormats()[112]) Done Setting [EndPoint]$Name" 
    }
}


function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [System.String[]]
        $Toolkit,

        [System.String]
        $SecurityDescriptorSddl,

        [System.String[]]
        $Group,

        [System.Boolean]
        $CleanAll,

        [ValidateSet('Present','Absent')]
        [System.String]
        $Ensure = 'Present'
    )

    try
    {
        if ($Cleanall)
        {
            return $false
        }else
        {
            Write-Verbose "Test [JeaEndPoint]$name"

            $endPoint = Get-PSSessionConfiguration -Name $Name -Verbose:0 -ErrorAction Stop
            $namePresentMessage = " [JeaEndPoint]$name Present"
            Write-Verbose -Message $namePresentMessage
            if ($Ensure -eq 'Absent')
            {
                return $false
            }
            else
            {
                Write-Verbose ' TODO: Check for Toolkits, StartupScript and UserAccount'
                return $true
            }
        }
    }catch [Microsoft.PowerShell.Commands.WriteErrorException]
    {
        Write-Verbose " [JeaEndPoint]$Name Absent"
        return ($Ensure -eq 'Absent')
    }
}


Export-ModuleMember -Function *-TargetResource