How to Find Inactive Computers in Active Directory Using PowerShell 

Understanding the Need for Identifying Inactive Computers in Active Directory

Inactive computers in AD (Active Directory) can pose risks to organizational security and efficiency. These machines may no longer exist physically but remain as dormant entries, creating clutter and potential entry points for threat actors. Regularly identifying and removing such entries helps ensure a clean and secure Active Directory environment.

This PowerShell script automates the task of locating computers that have been inactive for a specified number of days. It provides IT professionals with actionable insights and even integrates with NinjaOne’s custom fields for streamlined reporting.

The Script:

#Requires -Version 5.1

<#
.SYNOPSIS
    Gets computers that have been inactive for a specified number of days.
.DESCRIPTION
    Gets computers that have been inactive for a specified number of days.
    The number of days to consider a computer inactive can be specified as a parameter or saved to a custom field.

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

PARAMETER: -InactiveDays 30
    The number of days to consider a computer inactive. Computers that have been inactive for this number of days will be included in the report.
.EXAMPLE
    -InactiveDays 30
    ## EXAMPLE OUTPUT WITH InactiveDays ##
    [Info] Searching for computers that are inactive for 30 days or more.
    [Info] Found 11 inactive computers.

PARAMETER: -InactiveDays 30 -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField"
    The number of days to consider a computer inactive. Computers that have been inactive for this number of days will be included in the report.
.EXAMPLE
    -InactiveDays 30 -WysiwygCustomField "ReplaceMeWithAnyWysiwygCustomField"
    ## EXAMPLE OUTPUT WITH WysiwygCustomField ##
    [Info] Searching for computers that are inactive for 30 days or more.
    [Info] Found 11 inactive computers.
    [Info] Attempting to set Custom Field 'Inactive Computers'.
    [Info] Successfully set Custom Field 'Inactive Computers'!

.NOTES
    Minimum OS Architecture Supported: Windows Server 2016
    Release Notes: Initial Release
#>

[CmdletBinding()]
param (
    [Parameter()]
    $InactiveDays,
    [Parameter()]
    [String]$WysiwygCustomField
)

begin {
    function Test-IsElevated {
        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
        $p = New-Object System.Security.Principal.WindowsPrincipal($id)
        $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
    }
    function Set-NinjaProperty {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory = $True)]
            [String]$Name,
            [Parameter()]
            [String]$Type,
            [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
            $Value,
            [Parameter()]
            [String]$DocumentName
        )
    
        $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters
        if ($Characters -ge 10000) {
            throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.")
        }
        
        # If we're requested to set the field value for a Ninja document we'll specify it here.
        $DocumentationParams = @{}
        if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName }
        
        # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed.
        $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG"
        if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" }
        
        # The field below requires additional information to be set
        $NeedsOptions = "Dropdown"
        if ($DocumentName) {
            if ($NeedsOptions -contains $Type) {
                # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong.
                $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1
            }
        }
        else {
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1
            }
        }
        
        # If an error is received it will have an exception property, the function will exit with that error information.
        if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions }
        
        # The below types require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports.
        switch ($Type) {
            "Checkbox" {
                # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry.
                $NinjaValue = [System.Convert]::ToBoolean($Value)
            }
            "Date or Date Time" {
                # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID.
                $Date = (Get-Date $Value).ToUniversalTime()
                $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date
                $NinjaValue = $TimeSpan.TotalSeconds
            }
            "Dropdown" {
                # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid.
                $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
                $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID
        
                if (-not $Selection) {
                    throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown")
                }
        
                $NinjaValue = $Selection
            }
            default {
                # All the other types shouldn't require additional work on the input.
                $NinjaValue = $Value
            }
        }
        
        # We'll need to set the field differently depending on if its a field in a Ninja Document or not.
        if ($DocumentName) {
            $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1
        }
        else {
            $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1
        }
        
        if ($CustomField.Exception) {
            throw $CustomField
        }
    }
}
process {
    if (-not (Test-IsElevated)) {
        Write-Host "[Error] Access Denied. Please run with Administrator privileges."
        exit 1
    }

    # Get Script Variables and override parameters with them
    if ($env:inactiveDays -and $env:inactiveDays -notlike "null") {
        $InactiveDays = $env:inactiveDays
    }
    if ($env:wysiwygCustomField -and $env:wysiwygCustomField -notlike "null") {
        $WysiwygCustomField = $env:wysiwygCustomField
    }

    # Parameter Requirements
    if ([string]::IsNullOrWhiteSpace($InactiveDays)) {
        Write-Host "[Error] Inactive Days is required."
        exit 1
    }
    elseif ([int]::TryParse($InactiveDays, [ref]$null) -eq $false) {
        Write-Host "[Error] Inactive Days must be a number."
        exit 1
    }

    # Check that Active Directory module is available
    if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
        Write-Host "[Error] Active Directory module is not available. Please install it and try again."
        exit 1
    }

    try {
        # Get the date in the past $InactiveDays days
        $InactiveDate = (Get-Date).AddDays(-$InactiveDays)
        # Get the SearchBase for the domain
        $Domain = "DC=$(
            $(Get-CimInstance Win32_ComputerSystem).Domain -split "\." -join ",DC="
        )"
        Write-Host "[Info] Searching for computers that are inactive for $InactiveDays days or more."

        # For Splatting parameters into Get-ADComputer
        $GetComputerSplat = @{
            Property   = "Name", "LastLogonTimeStamp", "OperatingSystem"
            # LastLogonTimeStamp is converted to a DateTime object from the Get-ADComputer cmdlet
            Filter     = { (Enabled -eq "true") -and (LastLogonTimeStamp -le $InactiveDate) }
            SearchBase = $Domain
        }

        # Get inactive computers that are not active in the past $InactiveDays days
        $InactiveComputers = Get-ADComputer @GetComputerSplat | Select-Object "Name", @{
            # Format the LastLogonTimeStamp property to a human-readable date
            Name       = "LastLogon"
            Expression = {
                if ($_.LastLogonTimeStamp -gt 0) {
                    # Convert LastLogonTimeStamp to a datetime
                    $lastLogon = [DateTime]::FromFileTime($_.LastLogonTimeStamp)
                    # Format the datetime
                    $lastLogonFormatted = $lastLogon.ToString("MM/dd/yyyy hh:mm:ss tt")
                    return $lastLogonFormatted
                }
                else {
                    return "01/01/1601 00:00:00 AM"
                }
            }
        }, "OperatingSystem"

        if ($InactiveComputers -and $InactiveComputers.Count -gt 0) {
            Write-Host "[Info] Found $($InactiveComputers.Count) inactive computers."
        }
        else {
            Write-Host "[Info] No inactive computers were found."
        }
    }
    catch {
        Write-Host "[Error] Failed to get inactive computers. Please try again."
        exit 1
    }

    # Save the results to a custom field
    if ($WysiwygCustomField) {
        try {
            Write-Host "[Info] Attempting to set Custom Field '$WysiwygCustomField'."
            Set-NinjaProperty -Name $WysiwygCustomField -Value $($InactiveComputers | ConvertTo-Html -Fragment | Out-String)
            Write-Host "[Info] Successfully set Custom Field '$WysiwygCustomField'!"
        }
        catch {
            Write-Host "[Error] Failed to set Custom Field '$WysiwygCustomField'."
            $ExitCode = 1
        }
    }

    $InactiveComputers | Format-Table -AutoSize | Out-String -Width 4000 | Write-Host

    exit $ExitCode
}
end {
    
    
    
}

 

Save time with over 300+ scripts from the NinjaOne Dojo.

Get access today.

How the PowerShell Script Works

This script is designed for IT professionals managing AD environments and requires PowerShell 5.1 or higher. Here’s a detailed breakdown of its operation:

Prerequisites

  • Administrative Privileges: The script checks for elevated privileges, ensuring it runs securely and effectively.
  • Active Directory Module: The script relies on the Active Directory PowerShell module to interact with AD objects.
  • Custom Field Configuration (Optional): If using NinjaOne, the script can write results to a custom field for easier tracking.

Key Components

  1. Parameter Handling:
    The script accepts two parameters:
  2. -InactiveDays: Specifies the threshold (in days) for considering a computer inactive.
  3. -WysiwygCustomField: Optional parameter to save results into a custom field in NinjaOne.
  4. Environment Variable Overrides:
    The script can pull configuration values from environment variables, allowing dynamic adjustments without hardcoding parameters.
  5. Inactive Computer Search:
    Using Get-ADComputer, the script queries AD for computers whose LastLogonTimeStamp is older than the specified threshold. It converts the LastLogonTimeStamp to a human-readable format for better reporting.
  6. Custom Field Integration:
    If -WysiwygCustomField is specified, the script leverages a Set-NinjaProperty function to save the HTML-formatted results into NinjaOne’s custom fields.
  7. Error Handling:
    The script includes robust error checks for module availability, parameter validation, and result processing, ensuring smooth execution.

Practical Use Cases

Scenario: Cleaning Up a Dormant Network

An MSP managing a mid-sized enterprise network notices a spike in login failures and suspects inactive computer accounts in AD are contributing to security vulnerabilities. Using this script, the IT team quickly identifies computers inactive for over 60 days and exports the results for review. With these insights, they clean up the AD, improving network efficiency and security.

Scenario: Proactive Maintenance

A company implements the script as part of their quarterly maintenance routine, ensuring their Active Directory remains lean and up-to-date. The script’s integration with NinjaOne allows the IT team to maintain a historical log of inactive computers for auditing purposes.

How This Script Compares to Other Methods

Manual Queries:

While IT admins can manually query AD using PowerShell cmdlets like Get-ADComputer, this script automates the process, ensuring consistent results and reducing human error.

Third-Party Tools:

Commercial AD cleanup tools offer similar functionality but come at a cost. This script provides a free and customizable alternative, with the added benefit of integration with NinjaOne.

Built-In GUI Tools:

Using AD administrative tools for this task can be tedious and time-consuming. The script streamlines the process, especially for large environments.

Frequently Asked Questions

Can I use this script without NinjaOne?

Absolutely. The script works independently to identify inactive computers. The NinjaOne integration is optional.

How does the script determine inactivity?

It checks the LastLogonTimeStamp attribute of computer accounts and compares it to the specified threshold.

Is this script safe to use?

Yes, but ensure it runs in a secure environment with proper permissions. Test in a non-production environment if unsure.

Can I modify the script for other AD objects, like users?

Yes, with minor adjustments to the Get-ADComputer cmdlet, you can target users or other AD objects.

Implications of Using This Script

Cleaning up inactive computers reduces the attack surface of your network, minimizes clutter, and improves Active Directory performance. By regularly using this script, organizations can maintain tighter security and ensure compliance with IT governance policies.

Recommendations for Using This Script

  1. Run as Administrator: Ensure you execute the script with elevated privileges.
  2. Test in a Lab Environment: Before deploying in production, test the script to confirm expected behavior.
  3. Automate the Process: Schedule the script to run periodically using Task Scheduler or a similar tool.
  4. Use with NinjaOne: Leverage the integration to maintain comprehensive reports within your RMM platform.

Final Thoughts

Managing inactive computers in Active Directory is a vital task for IT professionals. This PowerShell script provides a robust, flexible solution, automating the identification process and integrating seamlessly with NinjaOne for enhanced reporting. By incorporating such tools into their workflows, IT teams can improve security, streamline operations, and maintain a clean Active Directory environment.

For IT professionals looking to simplify AD management, NinjaOne offers powerful tools that complement scripts like this, enabling streamlined monitoring and proactive maintenance.

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