Skip to content

Instantly share code, notes, and snippets.

@jdhitsolutions
Last active February 18, 2024 05:25
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jdhitsolutions/3bce157bd64717dd616b949f6e280433 to your computer and use it in GitHub Desktop.
Save jdhitsolutions/3bce157bd64717dd616b949f6e280433 to your computer and use it in GitHub Desktop.
A PowerShell function and custom format file for displaying changed objects in Active Directory.

Get-ADChange

The files in this gist are designed to query Active Directory for recently changed objects. The files should go in the same folder and make sure the .ps1xml format file is saved as adchange.format.ps1xml. You can then dot source the script file:

. c:\scripts\get-adchange.ps1

Obviously, use the appropriate path. The script will also load the format file into your PowerShell session. You should be able to run the Get-ADChange command from a Windows 10 desktop that has the ActiveDirectory module installed.

 Get-ADChange -Category user,group -Since 9:00AM -IncludeDeletedObjects

These files are explained at https://jdhitsolutions.com/blog/powershell/8097/building-a-powershell-tool-for-active-directory-changes/.

DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING.

<?xml version="1.0" encoding="UTF-8"?>
<!--
Format type data generated 01/26/2021 17:24:24 by COMPANY\ArtD
This file was created using the New-PSFormatXML command that is part
of the PSScriptTools module.
https://github.com/jdhitsolutions/PSScriptTools
-->
<Configuration>
<ViewDefinitions>
<View>
<!--Created 01/26/2021 17:24:24 by COMPANY\ArtD-->
<Name>default</Name>
<ViewSelectedBy>
<TypeName>ADChange</TypeName>
</ViewSelectedBy>
<GroupBy>
<PropertyName>ReportDate</PropertyName>
<Label>ReportDate</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.
<AutoSize />-->
<TableHeaders>
<TableColumnHeader>
<Label>DistinguishedName</Label>
<Width>45</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>WhenCreated</Label>
<Width>22</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>WhenChanged</Label>
<Width>22</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>New</Label>
<Width>5</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Deleted</Label>
<Width>7</Width>
<Alignment>center</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Class</Label>
<Width>8</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<!-- <Wrap /> -->
<TableColumnItems>
<TableColumnItem>
<ScriptBlock>
if ($host.name -eq 'ConsoleHost') {
<!-- I am adjusting the distinguished name value to take the ANSI formatting into account-->
if ($_.IsDeleted) {
$dn = "{0}..." -f ($_.DistinguishedName.substring(0,37))
"$([char]0x1b)[91m$dn$([char]0x1b)[0m"
}
elseif ($_.IsNew) {
$dn = "{0}..." -f ($_.DistinguishedName.substring(0,37))
"$([char]0x1b)[92m$dn$([char]0x1b)[0m"
}
else {
$_.DistinguishedName
}
} <!-- in the console -->
else {
$_.DistinguishedName
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>WhenCreated</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>WhenChanged</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if ($host.name -eq 'ConsoleHost') {
if ($_.IsNew) {
"$([char]0x1b)[92m$($_.IsNew)$([char]0x1b)[0m"
}
}
elseif ($_.IsNew) {
$_.IsNew
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if ($host.name -eq 'ConsoleHost') {
if ($_.IsDeleted) {
"$([char]0x1b)[91m$($_.IsDeleted)$([char]0x1b)[0m"
}
else {
$_.IsDeleted
}
}
else {
$_.IsDeleted
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
If ($_.ObjectClass -eq 'organizationalunit') {
"OU"
}
else {
$_.ObjectClass
}</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<!--Created 01/27/2021 09:10:43 by COMPANY\ArtD-->
<Name>container</Name>
<ViewSelectedBy>
<TypeName>ADChange</TypeName>
</ViewSelectedBy>
<GroupBy>
<PropertyName>Container</PropertyName>
<Label>Container</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.
<AutoSize />-->
<TableHeaders>
<TableColumnHeader>
<Label>DistinguishedName</Label>
<Width>50</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>WhenCreated</Label>
<Width>23</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>WhenChanged</Label>
<Width>23</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>IsNew</Label>
<Width>8</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>ObjectClass</Label>
<Width>14</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>DistinguishedName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>WhenCreated</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>WhenChanged</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>IsNew</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
If ($_.ObjectClass -eq 'organizationalunit') {
"OU"
}
else {
$_.ObjectClass
}</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<!--Created 01/27/2021 09:20:13 by COMPANY\ArtD-->
<Name>class</Name>
<ViewSelectedBy>
<TypeName>ADChange</TypeName>
</ViewSelectedBy>
<GroupBy>
<PropertyName>ObjectClass</PropertyName>
<Label>ObjectClass</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.
<AutoSize />-->
<TableHeaders>
<TableColumnHeader>
<Label>DistinguishedName</Label>
<Width>50</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>WhenCreated</Label>
<Width>23</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>WhenChanged</Label>
<Width>23</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>IsNew</Label>
<Width>8</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>IsDeleted</Label>
<Width>12</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>DistinguishedName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>WhenCreated</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>WhenChanged</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>IsNew</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>IsDeleted</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
#requires -version 5.1
#requires -module ActiveDirectory
# Learn more about PowerShell: http://jdhitsolutions.com/blog/essential-powershell-resources/
Function Get-ADChange {
[cmdletbinding()]
[outputtype("ADChange")]
[alias("gadc")]
Param (
[Parameter(Position = 0, HelpMessage = "Enter a last modified datetime for AD objects. The default is the last 4 hours.")]
[ValidateNotNullOrEmpty()]
[datetime]$Since = ((Get-Date).AddHours(-4)),
[Parameter(HelpMessage = "Specify the types of objects to query.")]
[ValidateSet("User","Group","Computer","OU")]
[ValidateNotNullOrEmpty()]
[string[]]$Category = "User",
[Parameter(HelpMessage = "Include deleted objects if the AD Recycle Bin feature has been enabled.")]
[switch]$IncludeDeletedObjects,
[Parameter(HelpMessage = "Specifies an Active Directory path to search under.")]
[string]$SearchBase,
[Parameter(HelpMessage = "Specifies the Active Directory Domain Services domain controller to query. The default is your Logon server.")]
[alias("DC")]
[ValidateNotNullorEmpty()]
#[ArgumentCompleter({(Get-ADDomain).ReplicaDirectoryServers})]
[string]$Server = $env:LOGONSERVER.SubString(2),
[Parameter(HelpMessage = "Specify an alternate credential for authentication.")]
[alias("runas")]
[pscredential]$Credential,
[ValidateSet("Negotiate", "Basic")]
[string]$AuthType
)
#an internal version number for this function
$ver = [version]"1.2.0"
Function _ConvertToLDAPTime {
#a private helper function to convert a date time object into a LDAP query-compatible value
Param([datetime]$Date)
$offset = (Get-TimeZone).baseUtcOffset
#values must be formatted with leading zeros to the specified number of decimal places
$tz = "{0:d2}{1:d2}" -f $offset.hours,$offset.Minutes
"{0:yyyyMMddhhmmss}.0{1}" -f $date,$tz
}
Write-Verbose "[$(Get-Date)] Starting $($MyInvocation.MyCommand) version $ver"
Write-Verbose "[$(Get-Date)] Using these bound parameters:"
$PSBoundParameters | Out-String | Write-Verbose
#display some runtime metadata with Verbose output
Write-Verbose "[$(Get-Date)] Running as: $($env:USERDOMAIN)\$($env:USERNAME) from $env:COMPUTERNAME"
Write-Verbose "[$(Get-Date)] Operating System: $(Get-ItemPropertyValue -path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\' -name ProductName)"
Write-Verbose "[$(Get-Date)] PowerShell: v$($PSVersionTable.PSVersion)"
#get the current datetime to use as a reporting date for the changed objects
$ReportDate = Get-Date
#build a hashtable of parameters to splat to Get-ADObject
$getParams = @{
ErrorAction = 'Stop'
Server = $Server
Properties = 'WhenCreated','WhenChanged','Description','DisplayName'
}
$params = "Credential", "AuthType","SearchBase","IncludeDeletedObjects"
ForEach ($param in $params) {
if ($PSBoundParameters.ContainsKey($param)) {
Write-Verbose "[$(Get-Date)] Adding parameter $param"
$getParams.Add($param,$PSBoundParameters.Item($param))
}
}
if ($Credential.username) {
Write-Verbose "[$(Get-Date)] Using alternate credentials for $($credential.username)"
}
if ($SearchBase) {
Write-Verbose "[$(Get-Date)] Searching from $SearchBase"
}
if ($IncludeDeleted) {
Write-Verbose "[$(Get-Date)] Including deleted items in the search"
}
Write-Verbose "[$(Get-Date)] Filtering for changed objects since '$since' from $($server.toUpper())"
#define a list to hold search results
$items = [System.Collections.Generic.list[object]]::new()
#convert the $Since value to an LDAP compatible value
$dt = _ConvertToLDAPTime -Date $Since
Try {
#go through each category
foreach ($objClass in $Category) {
<#
filtering on object types using an LDAP filter because Computer is derived from the User class
and I want to be able to distinguish between the two. Instead of a single complex filtering query based on
object types or categories, I'll just run the query for each requested objectclass.
#>
Switch ($objclass) {
"User" { $ldap = "(&(WhenChanged>=$dt)(objectclass=user)(!(objectclass=computer)))" }
"Computer" { $ldap = "(&(WhenChanged>=$dt)(objectclass=computer))"}
"Group" { $ldap = "(&(WhenChanged>=$dt)(objectclass=group))"}
"OU" { $ldap = "(&(WhenChanged>=$dt)(objectclass=organizationalunit))"}
}
Write-Verbose "[$(Get-Date)] Using LDAP filter $ldap"
$getparams["LDAPFilter"] = $ldap
Get-ADObject @getParams | Foreach-Object { $items.Add($_)}
}
}
Catch {
Write-Warning "[$(Get-Date)] Failed to query Active Directory. $($_.Exception.Message)."
}
if ($items.count -gt 0) {
Write-Verbose "[$(Get-Date)] Found $($items.count) items."
#add custom properties and insert a new type name
foreach ($item in $items) {
if ($item.WhenCreated -ge $since) {
$isNew = $True
}
else {
$isNew = $false
}
#create a custom object based on each search result
[PSCustomObject]@{
PSTypeName = "ADChange"
ObjectClass = $item.ObjectClass
ObjectGuid = $item.ObjectGuid
DistinguishedName = $item.DistinguishedName
Name = $item.Name
DisplayName = $item.DisplayName
Description = $item.Description
WhenCreated = $item.WhenCreated
WhenChanged = $item.WhenChanged
IsNew = $IsNew
IsDeleted = $item.Deleted
Container = $item.distinguishedname.split(",", 2)[1]
DomainController = $Server.toUpper()
ReportDate = $ReportDate
}
} #foreach item
}
else {
Write-Warning "[$(Get-Date)] No changed objects found that match your criteria."
}
Write-Verbose "[$(Get-Date)] Ending $($MyInvocation.MyCommand)"
} #end function
#define a default set of properties
Update-TypeData -TypeName ADChange -DefaultDisplayPropertySet DistinguishedName,WhenCreated,WhenChanged,IsNew,IsDeleted,ObjectClass,ReportDate -Force
#define some alias properties for the custom object
Update-TypeData -TypeName ADChange -MemberType AliasProperty -MemberName class -Value ObjectClass -Force
Update-TypeData -TypeName ADChange -MemberType AliasProperty -MemberName DN -Value DistinguishedName -Force
#load a custom formatting file which has additional custom views of container and class
#It is assumed the format file is in the same directory as this file.
Update-FormatData $PSScriptRoot\ADchange.format.ps1xml
MIT License
Copyright (c) 2020 JDH Information Technology Solutions, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment