Get-OldComputers
Getting a List of Old Computers in Active Directory (AD)
In the code below, I (essentially) define an "old computer" as one that hasn't changed it's AD password or logged-in to AD in the last 12 - 18 months. Change the code to match your definition.
If you don't want to keep entering your AD Domain every script execution, I'd change the Domain
parameter too.
If you allow delegated admins in your Domain, or if you just want to concentrate on a specific organization unit (OU), you can supply the OU
parameter in standard LDAP distinguishedName format.
If you are unfamiliar with using the .Net DataSet and DataTable for an in-memory database, you may find some of this code instructive. Certainly, using the DataTable's Select()
member function was far faster than using a PowerShell Where-Object
or a Contains
. By the way, putting the [void]
in front of many of the actions in the code prevents it from emitting whatever return value/object that action normally returns (I didn't want the return value anyway).
1<#
2.SYNOPSIS
3 Generate a report on computers that haven't changed their passwords or logged in the last 12 - 18 months.
4
5.DESCRIPTION
6 In an effort not to require the AD module, this code uses DirectoryServices.
7 The code uses your current AD identity to query AD.
8
9 ASSUMPTIONS:
10 - You are using a Domain joined machine in the Domain you want to query
11 - You are logged into the Domain you want to query with proper credentials
12 - The dsquery.exe EXE is in your path
13 - If a computer has logged in recently and pwdLastSet is out of date, they must have some
14 reason not to change their password
15
16.PARAMETER Domain
17 Defaults to your.domain (TODO: change it!)
18
19.PARAMETER OU
20 The search root instead of the top of the directory
21
22.PARAMETER IncludeBitlocker
23 If you have security rights to view Bitlocker information in your Domain, you can use this switch.
24 Otherwise, it will not generate an error and return the computer object data without bitlocker info.
25
26.EXAMPLE
27 Get-OldComputers.ps1 | Export-Csv -NotypeInformation -Encoding utf8 -Path .\computers.csv
28 Get-OldComputers.ps1 | Out-GridView
29
30.INPUTS
31 None
32
33.OUTPUTS
34 An array of DataRow objects that you can export to a CSV or whatever you want.
35
36.LINK
37 None
38
39.NOTES
40 Ver Date Who What
41 --- ---- --- ----
42 1.0 2024-02-14 Ray Initial
43 1.1 2024-02-15 Ray Added BitLocker and in-memory dataset/datatable
44 1.2 2024-02-15 Ray Modified documentation
45#>
46[CmdletBinding()]
47param (
48 [Parameter(Mandatory=$false)]
49 [string]$Domain='your.domain',
50 [Parameter(Mandatory=$false,HelpMessage='Enter something like: *OU=sub_ou,OU=parent_ou,DC=your,DC=domain')]
51 [string]$OU='',
52 [Parameter(Mandatory=$false,HelpMessage='Add this switch if you have security rights and wish to know')]
53 [switch]$IncludeBitlocker
54)
55
56Add-Type -AssemblyName System.DirectoryServices
57Add-Type -AssemblyName System.Data
58
59function GetBitLockerInfo {
60 # find all the bitlocker records
61 [string]$BitlockerFilter = '(&(&(objectClass=msFVE-RecoveryInformation)' +
62 '(msFVE-RecoveryPassword=*)))'
63 try {
64 $script:searcher.Filter = $BitlockerFilter
65 $script:searcher.SearchRoot = ''
66 $script:searcher.PropertiesToLoad.Clear()
67 # The msDS-ParentDistName contains the owner computer's distinguishedName
68 [void]$script:searcher.PropertiesToLoad.Add('msDS-ParentDistName')
69 [System.DirectoryServices.SearchResultCollection]$bits = $script:searcher.FindAll()
70 if ($null -ne $bits -and $bits.Count -gt 0) {
71 # Many computers have more than one bitlocker key.
72 # We only need to know if they have one or not.
73 [string]$select = [string]::Empty
74 foreach ($b in $bits) {
75 $select = "distinguishedName = '"
76 # The data may contain a "'" in it--replace it just in case.
77 # Since the AD query might contain several objects relating to the same computer
78 # I created this select query to ignore duplicates.
79 # TODO: Change this if you want to evaluate the bitlocker keys in more detail
80 $select += $b.Properties.Item('msds-parentdistname')[0].ToString().Replace("'","''")
81 $select += "' AND hasbitlocker = FALSE"
82 $row = $script:ds.Tables[$script:tbl].Select($select)
83 # Since the distinguishedName is the primary key (and unique), we'll never have more than one record
84 if ($row.Count -eq 1) {
85 $row.BeginEdit()
86 $row.Item(0).hasbitlocker = $true
87 $row.EndEdit()
88 }
89 }
90 }
91 else {
92 Write-Warning 'No BitLocker data found. Do you have permission to read it?'
93 }
94 }
95 catch {
96 throw
97 }
98 finally {
99 if ($bits) {
100 $bits.Dispose()
101 }
102 }
103}
104
105New-Variable -Name PAGE_SIZE -Value 100 -Option Constant
106[string[]]$PropertiesToLoad = @('distinguishedName','lastLogonTimeStamp','name','operatingSystem','pwdLastSet','sAMAccountName','whenCreated')
107
108
109[datetime]$Jan1970 = Get-Date -Year 1970 -Month 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0
110[datetime]$Midnight = Get-Date -Hour 0 -Minute 0 -Second 0 -Millisecond 0
111[datetime]$TwelveMonthsAgo = $Midnight.AddMonths(-12)
112[datetime]$EightteenMonthsAgo = $Midnight.AddMonths(-18)
113# TODO: These are arbitrary time limits. Change them to meet your requirements
114# Find all enabled computers with a password last set and a lastlogontimestamp
115# >= 12 months ago and less then 18 months ago
116[string]$TwelveMonthFilter = "(&" +
117 "(objectCategory=Computer)(objectClass=Computer)" +
118 "(!useraccountcontrol:1.2.840.113556.1.4.803:=2)" +
119 "(&" +
120 "(pwdLastSet<=$($TwelveMonthsAgo.ToFileTime()))" +
121 "(pwdLastSet>=$($EightteenMonthsAgo.AddSeconds(-1).ToFileTime()))" +
122 ")" +
123 "(&" +
124 "(lastLogonTimeStamp<=$($TwelveMonthsAgo.ToFileTime()))" +
125 "(lastLogonTimeStamp>=$($EightteenMonthsAgo.AddSeconds(-1).ToFileTime()))" +
126 ")" +
127 ")"
128
129# connect to the domain and create a directory searcher
130$RootDE = New-Object System.DirectoryServices.DirectoryEntry -ArgumentList "LDAP://$($Domain)"
131$script:searcher = New-Object System.DirectoryServices.DirectorySearcher -ArgumentList $RootDE
132try {
133 $script:searcher.Filter = $TwelveMonthFilter
134 $script:searcher.PageSize = $PAGE_SIZE
135 $script:searcher.SearchScope = 'Subtree' # search all sub OUs
136 $script:searcher.PropertiesToLoad.AddRange($PropertiesToLoad) # we only need a few properties
137 if ($OU -ne '') {
138 $dsq = dsquery.exe ou "$($OU)" -scope base
139 if ($null -eq $dsq) {
140 Write-Error "I could not find the OU '($OU)' supplied in the '$($Domain) Domain"
141 throw 'OU not found!'
142 }
143 else {
144 $script:searcher.SearchRoot = $OU
145 }
146 }
147 [System.DirectoryServices.SearchResultCollection]$SearchResult = $script:searcher.FindAll()
148 if ($SearchResult) {
149 [string]$tbl = 'comps'
150 $script:ds = New-Object System.Data.DataSet
151 [void]$script:ds.Tables.Add($tbl)
152 [void]$script:ds.Tables[$tbl].Columns.Add('distinguishedName', [string])
153 [void]$script:ds.Tables[$tbl].Columns.Add('lastLogonTimeStamp', [datetime])
154 [void]$script:ds.Tables[$tbl].Columns.Add('name', [string])
155 [void]$script:ds.Tables[$tbl].Columns.Add('operatingSystem', [string])
156 [void]$script:ds.Tables[$tbl].Columns.Add('ou', [string])
157 [void]$script:ds.Tables[$tbl].Columns.Add('pwdLastSet', [datetime])
158 [void]$script:ds.Tables[$tbl].Columns.Add('sAMAccountName', [string])
159 [void]$script:ds.Tables[$tbl].Columns.Add('whenCreated', [datetime])
160 [void]$script:ds.Tables[$tbl].Columns.Add('hasbitlocker', [bool])
161 $script:ds.Tables[$tbl].Columns['hasbitlocker'].DefaultValue = $false
162 $script:ds.Tables[$tbl].PrimaryKey = $script:ds.Tables[$tbl].Columns['distinguishedName']
163 Write-Information "Number of Computer Objects to possibly disable: $($SearchResult.Count)"
164 [int16]$si = -1
165 $o = New-Object PSObject -Property @{distinguishedName='';lastLogonTimeStamp=$Jan1970;name='';
166 operatingSystem='';ou='';pwdLastSet=$Jan1970;sAMAccountName='';whenCreated=$Jan1970;}
167 foreach ($result in $SearchResult) {
168 $o.distinguishedName = ''
169 $o.lastLogonTimeStamp = $Jan1970
170 $o.name = ''
171 $o.operatingSystem = ''
172 $o.ou = ''
173 $o.pwdLastSet = $Jan1970
174 $o.sAMAccountName = ''
175 $o.whenCreated = $Jan1970
176 $o.distinguishedName = $result.Properties.distinguishedname[0]
177 if ($null -ne $result.Properties.Item('lastlogontimestamp')) {
178 $o.lastLogonTimeStamp = [datetime]::FromFileTime($result.Properties.Item('lastlogontimestamp')[0])
179 }
180 $o.name = $result.Properties.Item('name')[0]
181 if ($result.Properties.Item('operatingsystem')) {
182 $o.operatingSystem = $result.Properties.Item('operatingsystem')[0]
183 }
184
185 # TODO: change this if your environment differs
186 $si = $result.path.IndexOf('OU=')
187 if ($si -eq -1) {
188 $si = $result.path.IndexOf('CN=Computers,')
189 }
190 $o.ou = $result.path.Substring($si)
191
192 if ($null -ne $result.Properties.Item('pwdlastset')) {
193 $o.pwdLastSet = [datetime]::FromFileTime($result.Properties.Item('pwdlastset')[0])
194 }
195 $o.sAMAccountName = $result.Properties.Item('samaccountname')[0]
196 $o.whenCreated = $result.Properties.Item('whencreated')[0]
197
198 # add the data to the in-memory database
199 $nr = $script:ds.Tables[$tbl].NewRow()
200 $nr['distinguishedName'] = $o.distinguishedName
201 $nr['lastLogonTimeStamp'] = $o.lastLogonTimeStamp
202 $nr['name'] = $o.name
203 $nr['operatingSystem'] = $o.operatingSystem
204 $nr['ou'] = $o.ou
205 $nr['pwdLastSet'] = $o.pwdLastSet
206 $nr['sAMAccountName'] = $o.sAMAccountName
207 $nr['whenCreated'] = $o.whenCreated
208 [void]$script:ds.Tables[$tbl].Rows.Add($nr)
209 }
210 if ($IncludeBitlocker) {
211 GetBitLockerInfo
212 }
213 # Return all the rows
214 $script:ds.Tables[$tbl].Select()
215 }
216}
217catch {
218 throw
219}
220finally {
221 if ($script:ds) {
222 $script:ds.Dispose()
223 }
224 # release our resources
225 if ($RootDE) {
226 $RootDE.Close()
227 $RootDE.Dispose()
228 }
229 if ($script:searcher) {
230 $script:searcher.Dispose()
231 }
232}