How to Detect User Idle Time Using PowerShell

Idle time detection on computers has been a critical aspect for IT professionals, especially in the era of heightened cybersecurity threats and resource management. As organizations rapidly digitize and users often remain logged in for extended periods, detecting user idle times becomes paramount for both security and operational reasons. In this post, we’ll deep dive into a PowerShell script designed to detect the longest idle time of users, making the task seamless and efficient.

Background

The ability to determine how long a user has been inactive can provide valuable insights for IT professionals and Managed Service Providers (MSPs). Whether it’s about freeing up resources, ensuring sessions don’t remain open to potential security threats, or even billing considerations for cloud service providers, having an effective and reliable method to determine user inactivity becomes essential. This particular script leverages PowerShell’s prowess to interact directly with the Windows OS and pull relevant data to gauge idle times.

The Script

#Requires -Version 5.1

<#
.SYNOPSIS
    Returns the longest idle time of any user logged in or for a specific user.
.DESCRIPTION
    Returns the longest idle time of any user logged in or for a specific user.
    If RDS(Remote Desktop Services) is installed and the RSAT tools for it as well,
     then this will get the idle time of each logged in user.
    For workstations and servers(with out RDS installed),
     this will get the current idle of the currently logged in user.
    If a user is logged in via the console and another is via the admin RDP session,
     then both will be considered as one user for calculating idle time.
.EXAMPLE
    No parameters needed.
    Returns the longest idle time of all users logged in.
.EXAMPLE
     -UserName "Fred"
    Returns the longest idle time of the user Fred.
.EXAMPLE
    PS C:> Get-User-Idle-Time.ps1 -UserName "Fred"
    Returns the longest idle time of the user Fred.
.OUTPUTS
    PSCustomObject[]
.NOTES
    Minimum OS Architecture Supported: Windows 10, Windows Server 2016
    Release Notes:
    Adds functions to get idle time from RDS and non-RDS computers.
By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use.
    Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. 
    Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. 
    Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. 
    Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. 
    Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. 
    Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. 
    EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).
.COMPONENT
    ManageUsers
#>

[CmdletBinding()]
param (
    # Specify one user on a Terminal Services Server, else leave blank for normal servers and workstations
    [Parameter(Mandatory = $false)]
    $UserName
)

begin {
    function Test-IsElevated {
        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
        $p = New-Object System.Security.Principal.WindowsPrincipal($id)
        if ($p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator))
        { Write-Output $true }
        else
        { Write-Output $false }
    }
    Function Get-QueryUser() {
        Param()
        $Result = @()
        # Replaces all occurrences of 2 or more spaces in a row with a single comma
        $Lines = @(query.exe user).foreach({ $(($_) -replace ('s{2,}', ',')) })
        if ($Lines.Count -gt 1) {
            $Header = $($Lines[0].split(',').trim())
            for ($i = 1; $i -lt $($Lines.Count); $i++) {
                $Res = "" | Select-Object $Header
                $Line = $($Lines[$i].split(',')).foreach({ $_.trim().trim('>') })
                # Accounts for disconnected users
                if ($Line.count -eq 5) {
                    $Line = @($Line[0], "$($null)", $Line[1], $Line[2], $Line[3], $Line[4] )
                }
                for ($j = 0; $j -lt $($Line.count); $j++) {
                    $Res.$($Header[$j]) = $Line[$j]
                }
                $Result += $Res
                Remove-Variable Res
            }
            return $Result
        }
        else {
            return $null
        }
    }

    Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.ComponentModel;

namespace GetLastUserInput
{
    public class GetLastUserInput
    {
        private struct LASTINPUTINFO
        {
            public uint cbSize;
            public uint dwTime;
        }
        private static LASTINPUTINFO lastInPutNfo;
        static GetLastUserInput()
        {
            lastInPutNfo = new LASTINPUTINFO();
            lastInPutNfo.cbSize = (uint)Marshal.SizeOf(lastInPutNfo);
        }
        [DllImport("User32.dll")]
        private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);

        /// <summary>
        /// Idle time in ticks
        /// </summary>
        /// <returns></returns>
        public static uint GetIdleTickCount()
        {
            return ((uint)Environment.TickCount - GetLastInputTime());
        }
        /// <summary>
        /// Last input time in ticks
        /// </summary>
        /// <returns></returns>
        public static uint GetLastInputTime()
        {
            if (!GetLastInputInfo(ref lastInPutNfo))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }
            return lastInPutNfo.dwTime;
        }
    }
}
"@
}

process {
    if (-not (Test-IsElevated)) {
        Write-Error -Message "Access Denied. Please run with Administrator privileges."
        exit 1
    }
    if ($(Get-Module -Name "RemoteDesktop") -and $(Get-RDServer -ErrorAction SilentlyContinue)) {
        try {
            $Sessions = Get-RDUserSession
            $Sessions | Select-Object UserName, IdleTime
        }
        catch {
            Write-Warning -Message "A Remote Desktop Services deployment does not exist on $env:COMPUTERNAME."
        }
    }
    else {
        Write-Warning -Message "Remote Desktop Services is not installed on this computer, Falling back to query user."
        $Results = Get-QueryUser
        if ($null -eq $Results) {
            Write-Host "No user(s) logged in."
            exit 0
        }
        # Parse query results and loop through each user
        $Results | ForEach-Object {
            $CurrentUser = $_.USERNAME
            # If UserName param is used, only filter that user; If UserName param isn't used, return all users
            if ($CurrentUser -like $UserName -or ([string]::IsNullOrEmpty($UserName) -or [string]::IsNullOrWhiteSpace($UserName))) {
                # Output a PowerShell Custom Object array
                [PSCustomObject]@{
                    UserName    = $CurrentUser
                    SessionName = $_.SESSIONNAME
                    Id          = $_.ID
                    State       = $_.STATE
                    LogonTime   = $_.'LOGON TIME'
                    IdleTime    = if ($_.'IDLE TIME' -like 'none') { 0 }else { $_.'IDLE TIME' }
                }
            }
        } | Sort-Object -Property IdleTime | Select-Object -Property UserName, @{
            # Modify IdleTime when it shows none
            Label      = "IdleTime"
            Expression = {
                New-TimeSpan -Start $(Get-Date) -End $(Get-Date).AddMilliseconds([GetLastUserInput.GetLastUserInput]::GetIdleTickCount())
            }
        }
    }
}

end {}

 

Access 300+ scripts in the NinjaOne Dojo

Get Access

Detailed Breakdown

  • Cmdlet Binding & Parameters: The script begins with a CmdletBinding, allowing it to be used as a cmdlet. It contains an optional parameter for a specific username.
  • Inner Functions:
  • Test-IsElevated checks if the script runs with administrative privileges.
  • Get-QueryUser interfaces with query.exe to fetch details about the current users.
  • External Library Integration: An external library (GetLastUserInput) is used to fetch the exact idle tick count.
  • Process Block: The core logic lies here.
  • Firstly, it checks if the script runs with admin rights.
  • If Remote Desktop Services (RDS) is installed, it fetches the idle time for each logged-in user.
  • If RDS is not present, it falls back to the query user approach and calculates idle time based on the last user input.

Potential Use Cases

Imagine you’re an IT admin for a mid-sized organization. You’ve noticed that many users leave their workstations on and logged in, even after office hours. Not only is this a security risk, but it’s also a drain on network and power resources. By deploying this script enterprise-wide, you can quickly determine which users are actively working and which workstations have been idle. With this information, you can enforce auto-logoff policies or send reminders to users to shut down their machines.

Comparisons

While there are third-party tools available that detect idle time, they often come with added bloat or may not have the granular detail required. Some might opt for querying the Windows Event Log for specific events, but that approach might miss nuances like a running video keeping the session active. This script provides a direct and customizable method, leveraging native Windows tools and commands.

FAQs

  • Is RDS required for this script?
    No, it’s designed to detect idle times both with and without RDS.
  • How accurate is the idle time calculation?
    It’s precise to the level of system ticks since the last user input.

Implications

Detecting idle time is not just about resource management. An open and idle session could be a potential vulnerability. Malicious actors, once inside the network, can hijack these sessions. Thus, timely detection and session management are crucial for IT security.

Recommendations

  • Always run the script with administrative privileges for accurate results.
  • Regularly update and maintain the script to cater to changes in the Windows environment.
  • Integrate with monitoring tools to get real-time alerts on extended idle sessions.

Final Thoughts

In the context of managing idle user sessions, tools like NinjaOne can further amplify the effectiveness by providing a unified platform to monitor, alert, and manage. Whether it’s about optimizing resources or tightening the security perimeter, understanding user behaviors through idle times can be a game-changer. Leveraging scripts like the one discussed can form the backbone of this endeavor, especially when combined with comprehensive IT management solutions.

Add a Comment

Your email address will not be published. Required fields are marked *

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service deliver tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Categories:

You might also like

×

See NinjaOne in action!

By submitting this form, I accept NinjaOne's privacy policy.

NinjaOne Terms & Conditions

By clicking the “I Accept” button below, you indicate your acceptance of the following legal terms as well as our Terms of Use:

  • Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms.
  • Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party.
  • Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library belonging to or under the control of any other software provider.
  • Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations.
  • Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks.
  • Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script.
  • EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).