PowerShell Function to Get Inactive Active Directory Users

PowerShell Function to Get Inactive Active Directory Users

For those of us familiar with most security compliance audits, one of the tasks we're well versed in is checking for users who have not logged in within a specified period of time, and either reporting on, or disabling those users (or both). In an effort at automating both of those, I began writing a script to discover inactive users in Active Directory, create a report listing them, which can be sent to administrators, but also, taking steps to auto-re-mediate the security risk but notifying each user before automatically disabling their account.

Because there's a number of steps to this script, many of which can easily be re-purposed for other scripts, I decided to advance my PowerShell scripting knowledge and begin to write functions to perform each task. Contained below is the function representing the first task, gather the required information.

Function Description

This function performs a couple of very simple tasks.
  • Queries Active Directory for any user accounts which have not been logged in to within a specified period of time.
  • Creates an XML Template, if dose not yet exist.
  • Populates an XML file containing the UserName, LastLogonDate, E-mail Address, and Expiration Date of each account meeting the criteria for being inactive.
This function only populates the report data. It doesn't update any information in Active Directory, or take any other actions. This information can then be used later to send notifications, and automatically disable accounts.  

Function fn_Get-InactiveADUsers
{
<#
.Synopsis
This function returns inactive users in Active Directory.
.Description
This function searches the Active Directory domain or OU tree specified to locate
any users who have not logged in within a specified period of time. (Default is 6
months.) This information is then written to an XML file to be used for reporting.
CREATED BY: England, Matthew (http://pmibluepapers.blogspot.com/2014/02/BP14021801.html)
.Example
fn_Get-InactiveADUsers -OU 'OU=Users,DC=domain,DC=domain,DC=com' -MaxAgeMonths 6 -XMLOutDirectory 'C:\Temp'
Example shows calling function using parameter names.
.Example
fn_Get-InactiveADUsers 'OU=Users,DC=domain,DC=domain,DC=com' 6 'C:\Temp'
Example shows calling function using positional parameters.
.Example
fn_Get-InactiveADUsers -OU 'OU=Users,DC=domain,DC=domain,DC=com'
Example uses default values.
.Parameter OU
This is the Distinguished Name (DN) of the OU you wish to limit the scope of 
the query to.
This is a required parameter. There is no default value.
.Parameter MaxAgeMonths
The number of months since the user last logged in to the account.
Default is 6 Months.
.Parameter XMLOutDirectory
Used to specifiy the output directory.
Default to the users My Documents folder.

#>
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$True,Position=1)]
        [string]$OU,

        [Parameter(Mandatory=$False,Position=2)]
        [string]$MaxAgeMonths = 6,

        [Parameter(Mandatory=$False,Position=3)]
        [string]$XMLOutDirectory = $env:USERPROFILE + "\Documents"
    )

    Set-Location $XMLOutDirectory
    #Load Required Modules
    Import-Module ActiveDirectory
    
    $EvalDate = (Get-Date).AddMonths(-$MaxAgeMonths)
    $TitleDate = Get-Date -UFormat "%Y%m%d"

    # ACTIVE DIRECTORY QUERY 
    $ary_InactiveADUsers = Get-ADUser -Filter {
        (ObjectClass -eq "user") -and 
        (Enabled -eq 'True') -and 
        (Name -notlike '*svc*') -and
        (PasswordNeverExpires -eq 'False') -and
        (LastLogonDate -lt $EvalDate)
        } -SearchBase $OU -Properties "LastLogonDate","AccountExpirationDate"
    
    $Domain = Get-ADDomain
    
    # CREATE XML TEMPLATE
    if (Test-Path "$XMLOutDirectory\IADUTemplate.xml")
    {
    Write-Verbose "Required XML Template Found..."
    }
    else
    {
    Write-Verbose "Required XML template not found..." 
    Write-Verbose "Creating XML Template..."
    $xml_InactiveADUsers = @'
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="IADUReport.xsl" ?>
<InactiveADUsers>
<ReportDate></ReportDate>
<Domain></Domain>
<User>
<SamAccountName></SamAccountName>
<LastLogonDate></LastLogonDate>
<mail></mail>
<givenName></givenName>
<AccountExpirationDate></AccountExpirationDate>
<DisabledDate></DisabledDate>
</User>
</InactiveADUsers>
'@

    $xml_InactiveADUsers | Out-File "$XMLOutDirectory\IADUTemplate.xml" -Encoding UTF8
    Write-Verbose "Required XML template InactiveADUsers.xml has been created."
    }
    # Create XML file for use by other functions. (Properties= UserName, LastLogonDate, ExpireDate)
    $xml = New-Object xml
    Write-Verbose "Loading XML Template..."
    $xml.Load("$XMLOutDirectory\IADUTemplate.xml")
    $xml.InactiveADUsers | ForEach-Object {$_.ReportDate = (Get-Date).toString()}
    $xml.InactiveADUsers | ForEach-Object {$_.Domain = $Domain.DNSRoot.toString()}
    
    $newxmlItem = (@($xml.InactiveADUsers.user)[0]).Clone()
    $ary_InactiveADUsers | ForEach-Object {
        $newxmlItem = $newxmlItem.clone()
        $newxmlItem.SamAccountName = $_.SamAccountName.toString()
        $newxmlItem.LastLogonDate = $_.LastLogonDate.toString()
        #Need to add error checking to ensure mail attribute is not null before it can be saved to XML.
        $newxmlItem.mail = $_.SamAccountName + ("@domain.com").toString()
        $newxmlItem.givenName = $_.givenName.toString()
        $newxmlItem.AccountExpirationDate = if ($_.AccountExpirationDate) 
            {Write-Verbose "AccountExpirationDate set on account. Using AccountExpirationDate..."
            $_.AccountExpirationDate.toString()
            } 
            Else 
            {Write-Verbose "No AccountExpirationDate set on account..."
            "None"
            }
        $xml.InactiveADUsers.AppendChild($newxmlItem) >$null  
     }

    # Remove null objects from xml file.    
    $xml.InactiveADUsers.user |
        Where-Object {$_.saMAccountName -eq ""} |
        ForEach-Object {[void]$xml.InactiveADUsers.RemoveChild($_) }
  
        
    $xml.Save("$XMLOutDirectory\InactiveADUsers$TitleDate.xml")
    Write-Host -ForegroundColor Green "Saved InactiveADUsers$TitleDate.xml to $XMLOutDirectory."
    Remove-Module ActiveDirectory
    Write-Host -ForegroundColor Green "Operation Complete!"
    Write-Host -ForegroundColor Green "Users Reported as Inactive are listed below and can be found in the report:$XMLOutDirectory\InactiveADUsers$TitleDate.xml "
    #Invoke-Item $XMLOutDirectory\InactiveADUsers$TitleDate.xml
    #return $ary_InactiveADUsers |Format-Table -Property Name,LastLogonDate,SamAccountName 
}

fn_Get-InactiveADUsers

Foot Notes:

  1. Function currently lacks error handling. Ensure no null values exist. Null values can not be saved to an XML element as a string.
  2. ln 104 - The string entered for $newxmlItem.mail should be modified to meet your needs. Either modify the "domain.com" string or use $_.mail.toString()
  3. Verify the Active Directory Query filter meets your needs, and returns the expected items. 
  4. $MaxAgeMonths is in Months not Days. AD Queries for accounts not logged in in past # months. If you need to be more granular, this can be changed to days.
  5. Content of XML template IADUTemplate.xml file is not verified. If the file exists, it will not be over written, however if the content structure is not correct, load or modify errors may result.   

Running the Function

There's several examples in the help comments of the function.

fn_Get-InactiveADUsers -OU 'OU=Users,DC=domain,DC=domain,DC=com' -MaxAgeMonths 6 -XMLOutDirectory 'C:\Temp'

It's important to realize, that I'm using the Active Directory module, so you need to run this on a machine which has that module installed. 

Because neither the Domain or Credentials are being passed to the Get-ADUser cmdlet, ensure you're logged in to a machine joined to the domain you want to query, using credentials with appropriate permissions.

The function can be scoped to a specific OU using the -OU parameter allowing for testing or exclusion of specific accounts falling outside of the specified OU. Only one OU can be specified.

Writing XML

One question I received from a peer, was regarding how I created the XML file used for the reporting.

I opted to use XML instead of a Text, HTML, or CSV file because of it's versatility. XML can easily be consumed by web sites, including SharePoint, Excel, PowerShell and many other applications.This enables anyone to consume the data this function extracts, in a manner best suited to their needs. (It's also a first for me, so I was able to gain a considerable amount of experience with this area of PowerShell, as well as with XML in general.) The caveat here, is that you will need a separate XSL stylesheet to make the report data readable. (That's for a follow up post.)

When researching methods for writing & modifying XML data from PowerShell I had a number of options. The first was to use .NET XML objects, however, for my needs, that seemed a little too complex. 

The next was to use the PowerShell ConvertTo-Xml cmdlet. While it's an extremely simple way to create XML content which can easily be reused within PowerShell, it resulted in an XML file which was a bit bloated, containing elements and property information which I didn't need. Also because of the sensitive nature of user accounts, I wanted to control exactly what was stored in a clear text file. 

The script will create the necessary XML template, so there is no need to create or stage files to support the script.

Naming Conventions

You'll notice that I have deviated from the standard cmdlet naming convention for my functions a bit. Each function I've created for this script begins with "fn_". Because there's a number of similarly named functions, I wanted to ensure there was no confusion between the functions I've written and those which may already exist.


As I mentioned, this is my first PowerShell script using functions, and reading, writing, and modifying XML. I'd love to hear your questions and feedback. Stay tuned for some follow on functions, which will use the information from this script to:

  • Notify Users of Account Status
  • Update/Disable the Account
  • Delete the Users Network Share.

No comments :

Post a Comment