Add this script to your task scheduler or Jenkins job to automatically log off Idling sessions on a list of servers
# IdleRdpSessionControl.ps1
# version 0.02
# Windows machines to monitor
$computerThresholds=@(
[pscustomobject]@{
computername='server1.intranet.kimconnect.com'
idleHoursThreshold=120
},
[pscustomobject]@{
computername='server2.intranet.kimconnect.com'
idleHoursThreshold=3
}
)
$forcedLogoff=$true
# Obtain credentials being passed by Jenkins
$runasUser='domain\username'
$runasPassword='password'
$encryptedPassword=ConvertTo-SecureString $runasPassword -AsPlainText -Force
$runasCredentials=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $runasUser,$encryptedPassword
# Email relay parameters
$emailFrom=''
$emailTo=''#
$subject='Idle RDP Sessions Report'
$smtpRelayServer=''
function logOffRdpSession{
param(
$serverName=$env:computername,
$username,
$idleMinutes,
$runasCredentials
)
$username=if($username -match '\\'){[regex]::match($username,'\\(.*)$').captures.groups.value[1]
}else{
$username.tostring()
}
$sessions=if(!$runasCredentials){
qwinsta /server:$serverName
}else{
invoke-command -computername $servername -credential $runasCredentials -scriptblock {qwinsta /server:$env:computername}
}
$sessionId=.{
$sessionMatch=$sessions|?{$_ -match $username}
if($sessionMatch){
$array=$sessionMatch -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split ' '
return $array|?{$_.tostring() -match '\d+'}
}else{
return $null
}
}
if($sessionId){
try{
if(!$runasCredentials){
$sessionId|%{rwinsta $_ /SERVER:$serverName}
$sessions=qwinsta /server:$serverName
$newSessionId=.{
$sessionMatch=$sessions|?{$_ -match $username}
if($sessionMatch){
$array=$sessionMatch -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split ' '
return $array[2]
}else{
return $null
}
}
}else{
invoke-command -computername $servername -credential $runasCredentials -scriptblock{
param($sessionId,$username)
$sessionId|%{rwinsta $_ /SERVER:$env:computername}
$sessions=qwinsta /server:$env:computername
$newSessionId=.{
$sessionMatch=$sessions|?{$_ -match $username}
if($sessionMatch){
$array=$sessionMatch -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split ' '
return $array[2]
}else{
return $null
}
}
return $newSessionId
} -Args $sessionId,$username
}
}catch{
write-warning $_
}
if(!$newSessionId){
write-host "$username RDP session ID $sessionId on $serverName has been forcefully disconnected due to its idling of $idleMinutes minutes."
return $true
}else{
write-warning "$username RDP session ID $sessionId still exists on $serverName"
return $false
}
}else{
write-host "$username doesn't have an RDP session on $serverName"
return $true
}
}
# logOffRdpSession $computerName $username $runasCredentials
$allSessions=getSessionsInfo $computerThresholds.computername $runasCredentials
$targetSessions=$allSessions|?{$x=$_.computername;[int]$_.IdleMinutes -ge [int](($computerThresholds|where computername -eq $x).idleHoursThreshold*60)}
#$logoffSessions=@()
if($forcedLogoff){
foreach($session in $targetSessions){
$result=logOffRdpSession $session.ComputerName $session.UserName $session.IdleMinutes $runasCredentials
if($result){
$targetSessions=$targetSessions|?{$_ -ne $session}
#$logoffSessions+=$sessions
}
}
}
$sessionsToEmail=$targetSessions # future development: add a routine to check whether current report is identical to previous
if($null -ne $sessionsToEmail){
$css="
<style>
.h1 {
font-size: 18px;
height: 40px;
padding-top: 80px;
margin: auto;
text-align: center;
}
.h5 {
font-size: 22px;
text-align: center;
}
.th {text-align: center;}
.table {
padding:7px;
border:#4e95f4 1px solid;
background-color: white;
margin-left: auto;
margin-right: auto;
width: 100%
}
.colgroup {}
.th { background: #0046c3; color: #fff; padding: 5px 10px; }
.td { font-size: 11px; padding: 5px 20px; color: #000;
width: 1px;
white-space: pre;
}
.tr { background: #b8d1f3;}
.tr:nth-child(even) {
background: #dae5f4;
width: 1%;
white-space: nowrap
}
.tr:nth-child(odd) {
background: #b8d1f3;
width: 1%;
white-space: nowrap
}
pre code {
background-color: #eee;
border: 1px solid #999;
display: block;
padding: 20px;
}
</style>
"
$howToLogOffSessionCode="<pre><code>function logOffRdpSession"+${function:logOffRdpSession}+"}`r`n`r`nlogOffRdpSession SERVERNAME USERNAME</code></pre>"
$sessionsReformatted=$sessionsToEmail|select ComputerName,UserName,@{n='sessionId';e={$_.id}},IdleMinutes,State,LogonTime
$currentReport=$sessionsReformatted|ConvertTo-Html -Fragment|Out-String
$currentReportHtml=$currentReport -replace '\<(?<item>\w+)\>','<${item} class=''${item}''>'
$howToLogOffSessionCode=$howToLogOffSessionCode -replace '\<(?<item>\w+)\>','<${item} class=''${item}''>'
$emailContent='<html><head>'+$css+"</head><body><h5 class='h5'>$subject</h5>"+$currentReportHtml+"<br><br><h1 class='h1'>Function to forcefully log off a session</h1>"+$howToLogOffSessionCode+'</body></html>'
Send-MailMessage -From $emailFrom `
-To $emailTo `
-Subject $subject `
-Body $emailContent `
-BodyAsHtml `
-SmtpServer $smtpRelayServer
}else{
write-host "No idle sessions to notify Admins."
}
Old Version
$computernames=@(
'LAX-RDSNODE01'
)
$idleDaysThreshold=3
$forcedLogoff=$true
# Obtain credentials being passed by Jenkins
$runasUser=$env:runasUser
$runasPassword=$env:runasPassword
$encryptedPassword=ConvertTo-SecureString $runasPassword -AsPlainText -Force
$runasCredentials=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $runasUser,$encryptedPassword
# Email relay parameters
$emailFrom='[email protected]'
$emailTo='[email protected]','[email protected]'
$subject='Idling RDP Sessions Report'
$smtpRelayServer='relay02.dragoncoin.com'
function getSessionsInfo([string[]]$computernames=$env:computername,$runasCredentials){
$results=@()
function checkPorts($servers,$ports){
function includePortQry{
if (!(Get-Command portqry.exe -ErrorAction SilentlyContinue)){
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))}
choco install portqry -y
if (Get-Command portqry.exe -ErrorAction SilentlyContinue){return $true}else{return $false}
}else{
return $true
}
}
$portQryExists=includePortQry
if(!$portQryExists){
write-warning "Unable to proceed without portqry"
return $false
}
$results=@()
foreach ($remoteComputer in $servers){
#$ip=[System.Net.Dns]::GetHostAddresses($remoteComputer).IPAddressToString|select -first 1
write-host "Now scanning $remoteComputer"
$result=@()
foreach ($item in $ports){
$port=$item.port
$protocol=$item.protocol
$reachable=if($port -ne 135){
$command={
param($remoteComputer,$protocol,$port)
$command="portqry -n $remoteComputer -p $protocol -e $port|find ': LISTENING'"
invoke-expression $command
}
$jobId=(Start-Job $command -Args $remoteComputer,$protocol,$port).Id
$startCount=0;$waitSeconds=5
do{
$jobStatus=(get-job -id $jobId).State
if($jobStatus -eq 'Completed'){
$jobResult=receive-job $jobId
}else{
if($startCount++ -eq $waitSeconds){
$jobStatus='Completed'
$null=remove-job -id $jobId -force
$jobResult=$false
}else{
sleep 1
}
}
}until($jobStatus -eq 'Completed')
[bool]($jobResult)
}else{
[bool](portqry -n $remoteComputer -p $protocol -e $port|find 'Total endpoints found: ')
}
write-host "$port/$protocol : $reachable"
$result+=[PSCustomObject]@{
computername=$remoteComputer
port=$port
protocol=$protocol
reachable=$reachable
}
}
#$resultString=$results.GetEnumerator()|sort-object {[int]$_.Name}|out-string
$results+=$result
$resultString=$result|sort-object -property port|out-string
write-host $resultString
}
return $results
}
function getDisconnectedSessionInfo($line,$computer){
# convert multiple spaces into single space and split pieces into array
$thisLine = $line.Trim() -Replace '\s+',' ' -Split '\s'
$properties = @{
UserName = $thisLine[0]
ComputerName = $computer
}
$properties.SessionName = $null
$properties.Id = $thisLine[1]
$properties.State = $thisLine[2]
$properties.IdleMinutes=.{[string]$x=$thisLine[3]
switch -regex ($x){
'\.' {0;break}
'\+' {$dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
$hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
$dayMinutes+$hourMinutes
break;
}
'\:' {try{
([TimeSpan]::Parse($x)).totalMinutes
}catch{
"Invalid value: $x"
}
break
}
default {$x}
}
}
$properties.LogonTime = $thisLine[4..6] -join ' '
$result=New-Object -TypeName PSCustomObject -Property $properties
return $result
}
function getConnectedSessionInfo($line,$computer){
$thisLine = $line.Trim() -Replace '\s+',' ' -Split '\s'
$properties = @{
UserName = $thisLine[0]
ComputerName = $computer
}
$properties.SessionName = $thisLine[1]
$properties.Id = $thisLine[2]
$properties.State = $thisLine[3]
$properties.IdleMinutes=.{$x=$thisLine[4]
switch -regex ($x){
'\.' {0;break}
'\+' {$dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
$hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
$dayMinutes+$hourMinutes
break;
}
'\:' {try{
([TimeSpan]::Parse($x)).totalMinutes
}catch{
"Invalid value: $x"
}
break
}
default {$x}
}
}
$properties.LogonTime = $thisLine[5..($thisLine.GetUpperBound(0))] -join ' '
$result=New-Object -TypeName PSCustomObject -Property $properties
return $result
}
foreach ($computer in $computernames){
if((checkPorts $computer @{protocol='tcp';port=135}).reachable){
try {
# Perusing legacy commandlets as there are no PowerShell equivalents at this time
$sessions=if($runasCredentials){
invoke-command -computername $computer -credential $runasCredentials {quser /server:$computer 2>&1 | Select-Object -Skip 1}
}else{
quser /server:$computer 2>&1 | Select-Object -Skip 1
}
ForEach ($session in $sessions) {
$result=$null
$thatLine = $session.Trim() -Replace '\s+',' ' -Split '\s'
if ($thatLine[2] -eq 'Disc'){
$result=getDisconnectedSessionInfo $thatLine $computer
}elseif(!$onlyDisconnected){
$result=getConnectedSessionInfo $thatLine $computer
}
if($result){$results+=$result}
}
}catch{
$results+=New-Object -TypeName PSCustomObject -Property @{
ComputerName=$computer
Error=$_.Exception.Message
} | Select-Object -Property UserName,ComputerName,SessionName,Id,State,IdleMinutes,LogonTime,Error|sort -Property UserName
}
}else{
$results+=New-Object -TypeName PSCustomObject -Property @{
ComputerName=$computer
Error=$_.Exception.Message
} | Select-Object -Property UserName,ComputerName,SessionName,Id,State,IdleMinutes,LogonTime,Error|sort -Property UserName
}
}
return $results
}
#getSessionsInfo $computernames $runasCredentials
function logOffRdpSession{
param(
$serverName=$env:computername,
$username,
$idleMinutes,
$runasCredentials
)
$username=if($username -match '\\'){[regex]::match($username,'\\(.*)$').captures.groups.value[1]
}else{
$username.tostring()
}
$sessions=if(!$runasCredentials){
qwinsta /server:$serverName
}else{
invoke-command -computername $servername -credential $runasCredentials -scriptblock {qwinsta /server:$env:computername}
}
$sessionId=.{
$sessionMatch=$sessions|?{$_ -match $username}
if($sessionMatch){
$array=$sessionMatch -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split ' '
return $array|?{$_.tostring() -match '\d+'}
}else{
return $null
}
}
if($sessionId){
if(!$runasCredentials){
$sessionId|%{rwinsta $_ /SERVER:$serverName}
$sessions=qwinsta /server:$serverName
$newSessionId=.{
$sessionMatch=$sessions|?{$_ -match $username}
if($sessionMatch){
$array=$sessionMatch -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split ' '
return $array[2]
}else{
return $null
}
}
}else{
invoke-command -computername $servername -credential $runasCredentials -scriptblock{
param($sessionId,$username)
$sessionId|%{rwinsta $_ /SERVER:$env:computername}
$sessions=qwinsta /server:$env:computername
$newSessionId=.{
$sessionMatch=$sessions|?{$_ -match $username}
if($sessionMatch){
$array=$sessionMatch -replace '(^\s+|\s+$)','' -replace '\s+',' ' -split ' '
return $array[2]
}else{
return $null
}
}
} -Args $sessionId,$username
}
if(!$newSessionId){
write-host "$username RDP session ID $sessionId on $serverName has been forcefully disconnected due to its idling $idleMinutes minutes."
return $true
}else{
write-warning "$username RDP session ID $sessionId still exists on $serverName"
return $false
}
}else{
write-host "$username doesn't have an RDP session on $serverName"
return $true
}
}
# logOffRdpSession $computerName $username $runasCredentials
$allSessions=getSessionsInfo $computernames $runasCredentials
$targetSessions=$allSessions|?{[Int]$_.IdleMinutes -ge $idleDaysThreshold*1440}
#$logoffSessions=@()
if($forcedLogoff){
foreach($session in $targetSessions){
$result=logOffRdpSession $session.ComputerName $session.UserName $session.IdleMinutes $runasCredentials
if($result){
$targetSessions=$targetSessions|?{$_ -ne $session}
#$logoffSessions+=$sessions
}
}
}
$sessionsToEmail=$targetSessions # future development: add a routine to check whether current report is identical to previous
if($null -ne $sessionsToEmail){
$css="
<style>
.h1 {
font-size: 18px;
height: 40px;
padding-top: 80px;
margin: auto;
text-align: center;
}
.h5 {
font-size: 22px;
text-align: center;
}
.th {text-align: center;}
.table {
padding:7px;
border:#4e95f4 1px solid;
background-color: white;
margin-left: auto;
margin-right: auto;
width: 100%
}
.colgroup {}
.th { background: #0046c3; color: #fff; padding: 5px 10px; }
.td { font-size: 11px; padding: 5px 20px; color: #000;
width: 1px;
white-space: pre;
}
.tr { background: #b8d1f3;}
.tr:nth-child(even) {
background: #dae5f4;
width: 1%;
white-space: nowrap
}
.tr:nth-child(odd) {
background: #b8d1f3;
width: 1%;
white-space: nowrap
}
pre code {
background-color: #eee;
border: 1px solid #999;
display: block;
padding: 20px;
}
</style>
"
$howToLogOffSessionCode="<pre><code>function logOffRdpSession"+${function:logOffRdpSession}+"}`r`n`r`nlogOffRdpSession SERVERNAME USERNAME</code></pre>"
$sessionsReformatted=$sessionsToEmail|select ComputerName,UserName,@{n='sessionId';e={$_.id}},IdleMinutes,State,LogonTime
$currentReport=$sessionsReformatted|ConvertTo-Html -Fragment|Out-String
$currentReportHtml=$currentReport -replace '\<(?<item>\w+)\>','<${item} class=''${item}''>'
$howToLogOffSessionCode=$howToLogOffSessionCode -replace '\<(?<item>\w+)\>','<${item} class=''${item}''>'
$emailContent='<html><head>'+$css+"</head><body><h5 class='h5'>$subject</h5>"+$currentReportHtml+"<br><br><h1 class='h1'>Function to forcefully log off a session</h1>"+$howToLogOffSessionCode+'</body></html>'
Send-MailMessage -From $emailFrom `
-To $emailTo `
-Subject $subject `
-Body $emailContent `
-BodyAsHtml `
-SmtpServer $smtpRelayServer
}else{
write-host "No idle sessions to notify Admins."
}
Categories: