SHOW:
|
|
- or go back to the newest paste.
1 | #################################################### | |
2 | # | |
3 | # AD account cleanup script | |
4 | # Date Created : 9/22/17 | |
5 | # Author : PowerMonkey500 | |
6 | # | |
7 | #################################################### | |
8 | # | |
9 | # WARNING: THIS SCRIPT WILL OVERWRITE EXTENSIONATTRIBUTE3 FOR INACTIVE USERS, MAKE SURE YOU ARE NOT USING IT FOR ANYTHING ELSE | |
10 | # This script is SLOW because it gets the most accurate last logon possible by comparing results from all DCs. By default the lastlogontimestamp is only replicated every 14 days minus a random percentage of 5. | |
11 | # | |
12 | #################################################### | |
13 | ||
14 | #FUNCTION DECLARATIONS | |
15 | #=================================================================================== | |
16 | ||
17 | #Logging function - starts transcript and cleans logs older than specified retention date. | |
18 | Function Start-Logging{ | |
19 | param ( | |
20 | [Parameter(Mandatory=$true)][String]$LogDirectory, | |
21 | [Parameter(Mandatory=$true)][String]$LogName, | |
22 | [Parameter(Mandatory=$true)][Int]$LogRetentionDays | |
23 | ) | |
24 | ||
25 | #Sets screen buffer from 120 width to 500 width. This stops truncation in the log. | |
26 | $ErrorActionPreference = 'SilentlyContinue' | |
27 | $pshost = get-host | |
28 | $pswindow = $pshost.ui.rawui | |
29 | ||
30 | $newsize = $pswindow.buffersize | |
31 | $newsize.height = 3000 | |
32 | $newsize.width = 500 | |
33 | $pswindow.buffersize = $newsize | |
34 | ||
35 | $newsize = $pswindow.windowsize | |
36 | $newsize.height = 50 | |
37 | $newsize.width = 500 | |
38 | $pswindow.windowsize = $newsize | |
39 | $ErrorActionPreference = 'Continue' | |
40 | ||
41 | #Create log directory if it does not exist already | |
42 | If (!(Test-Path $LogDirectory)){mkdir $LogDirectory} | |
43 | ||
44 | #Starts logging. | |
45 | New-Item -ItemType directory -Path $LogDirectory -Force | Out-Null | |
46 | $Today = Get-Date -Format M-d-y | |
47 | Start-Transcript -Append -Path ($LogDirectory + "\" + $LogName + "." + $Today + ".log") | Out-Null | |
48 | ||
49 | #Shows proper date in log. | |
50 | Write-Output ("Start time: " + (Get-Date)) | |
51 | ||
52 | #Purges log files older than X days | |
53 | $RetentionDate = (Get-Date).AddDays(-$LogRetentionDays) | |
54 | Get-ChildItem -Path $LogDirectory -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $RetentionDate } | Remove-Item -Force | |
55 | } | |
56 | ||
57 | ###NOT USED IN SCRIPT, JUST FOR UTILITY WHEN TWEAKING### | |
58 | Function Get-ADUserLastLogon ([string]$UserName){ | |
59 | $dcs = Get-ADDomainController -Filter {Name -like "*"} | |
60 | $time = 0 | |
61 | $dt = 0 | |
62 | foreach($dc in $dcs) { | |
63 | Try | |
64 | { | |
65 | $user = Get-ADUser -Server $dc -identity $userName -properties LastLogon,LastLogonTimestamp | |
66 | if($user.LastLogonTimeStamp -gt $time) | |
67 | { | |
68 | $time = $user.LastLogonTimeStamp | |
69 | } | |
70 | if ($user.LastLogon -gt $time) | |
71 | { | |
72 | $time = $user.LastLogon | |
73 | } | |
74 | } | |
75 | Catch {} | |
76 | } | |
77 | $dt = [DateTime]::FromFileTime($time) | |
78 | ||
79 | $OutputObj = New-Object -TypeName PSObject | |
80 | $OutputObj | Add-Member -MemberType NoteProperty -Name SamAccountName -Value $user.SamAccountName | |
81 | $OutputObj | Add-Member -MemberType NoteProperty -Name Enabled -Value $user.Enabled | |
82 | $OutputObj | Add-Member -MemberType NoteProperty -Name LastLogon -Value $dt | |
83 | $OutputObj | Add-Member -MemberType NoteProperty -Name GivenName -Value $user.GivenName | |
84 | $OutputObj | Add-Member -MemberType NoteProperty -Name Surname -Value $user.SurName | |
85 | $OutputObj | Add-Member -MemberType NoteProperty -Name Name -Value $user.Name | |
86 | $OutputObj | Add-Member -MemberType NoteProperty -Name DistinguishedName -Value $user.DistinguishedNameName | |
87 | ||
88 | Return $OutputObj | |
89 | } | |
90 | ||
91 | #----------------------------------- | |
92 | #Main script | |
93 | #----------------------------------- | |
94 | #Declarations | |
95 | $From = "noreply@email.com" | |
96 | $SMTPServer = "server.domain.local" | |
97 | $To = @('email1@email.com','email2@email.com') #Array. You can add more than one entry. | |
98 | $Subject = "Account Cleanup Report" | |
99 | $TryCount = 0 #Leave this at 0. | |
100 | $LogDirectory = "C:\Disable-InactiveADAccountsLog" #No trailing slash | |
101 | $LogName = "Disable-InactiveADAccountsLog" | |
102 | $MaxTryCount = 20 #Amount of times to try for identical DC results before giving up. 30 second retry delay after each failure. | |
103 | $UTCSkew = -5 #Accounting for the time zone difference, since some results are given in UTC. Eastern time is UTC-5. | |
104 | $ExclusionsGroup = "ServiceAccts" #AD group containing accounts to exclude. | |
105 | $DaysThreshold = 30 #Threshold of days of inactivity before disabling the user. | |
106 | ||
107 | #Start logging | |
108 | Start-Logging -logdirectory $LogDirectory -logname $LogName -LogRetentionDays 30 | |
109 | ||
110 | #Get all DCs, add array names to vars array | |
111 | $DCnames = @() | |
112 | If (!$DCs){$DCs = Get-ADGroupMember 'Domain Controllers'} | |
113 | $DCs | % {$DCnames += $_.Name} | |
114 | ||
115 | #Check that results match from each DC | |
116 | While (($ComparisonResults -contains $False -or !$ComparisonResults) -and $TryCount -lt $MaxTryCount){ | |
117 | #Fetch AD users from each DC, add to named array | |
118 | $DCnames | % { | |
119 | #Filters / Exclusions. | |
120 | Write-Output ("Fetching last logon times from " + $_ + "...") | |
121 | New-Variable -Name $_ -Value (Get-ADUser -Filter {Enabled -eq $True} -Server $_ -Properties DistinguishedName,LastLogon,LastLogonTimestamp,whenCreated,Description | Sort SamAccountName) -Force | |
122 | } | |
123 | ||
124 | $ComparisonResults = @() | |
125 | ForEach ($i in 0..(($DCnames.Count)-1)){ | |
126 | If ($i -le (($DCnames.Count)-2)){ | |
127 | Write-Output ("Comparing results from " + $DCnames[$i] + " and " + $DCnames[$i+1] + "...") | |
128 | $NotEqual = Compare-Object (Get-Variable -Name $DCnames[$i]).Value (Get-Variable -Name $DCnames[$i+1]).Value -Property SamAccountName | |
129 | ||
130 | If (!$NotEqual) {$ComparisonResults += $True} | |
131 | Else {$ComparisonResults += $False} | |
132 | } | |
133 | } | |
134 | If ($ComparisonResults -contains $False){ | |
135 | Write-Warning "One or more DCs returned differing results. This is likely just replication delay. Retrying..." | |
136 | $TryCount++ | |
137 | Start-Sleep 30 | |
138 | } | |
139 | } | |
140 | If ($TryCount -lt $MaxTryCount){Write-Output "All DC results are identical!"} | |
141 | Else {Throw "Try limit exceeded. Aborting."} | |
142 | ||
143 | #Get current time for comparison later. | |
144 | If (!$StartTime){$StartTime = Get-Date} | |
145 | ||
146 | #User count so we know how many times to loop. | |
147 | $UserCount = (Get-Variable -Name $DCnames[0]).Value.Count | |
148 | ||
149 | #Create results array of the same size | |
150 | $FullResults = @($null) * $UserCount | |
151 | ||
152 | #Loop through array indexes | |
153 | ForEach ($i in 0..($UserCount -1)){ | |
154 | #Grab user object from each resultant array, make array of each user object | |
155 | $UserEntries = @(0) *$DCnames.Count | |
156 | ForEach ($o in 0..($DCnames.Count -1)) { | |
157 | $UserEntries[$o] = (Get-Variable -Name $DCnames[$o]).Value[$i] | |
158 | } | |
159 | If (($UserEntries.SamAccountName | Select -Unique).Count -gt 1){Throw "A user mismatch at index $i has occurred. Aborting."} | |
160 | ||
161 | #Find most recent LastLogon, whenCreated, and LastLogonTimestamp. | |
162 | If ($UserEntries.LastLogon){ | |
163 | [datetime]$LastLogon = [datetime]::FromFileTimeUtc(($UserEntries | Measure-Object -Property LastLogon -Maximum).Maximum) | |
164 | [datetime]$TrueLastLogon = $LastLogon | |
165 | } | |
166 | Else {[datetime]$LastLogon = 0; $TrueLastLogon = 0} | |
167 | ||
168 | If ($UserEntries.whenCreated){ | |
169 | [datetime]$whenCreated = $UserEntries[0].whenCreated | |
170 | } | |
171 | Else {[datetime]$whenCreated = 0} | |
172 | ||
173 | If ($UserEntries.LastLogonTimestamp){ | |
174 | [datetime]$LastLogonTimestamp = [datetime]::FromFileTimeUtc(($UserEntries | Measure-Object -Property LastLogonTimestamp -Maximum).Maximum) | |
175 | } | |
176 | Else {[datetime]$LastLogonTimestamp = 0} | |
177 | ||
178 | #If LastLogonTimestamp is newer, use that instead of LastLogon. | |
179 | If ($LastLogonTimestamp -gt $LastLogon){$TrueLastLogon = $LastLogonTimestamp} | |
180 | ||
181 | #UTC to EST | |
182 | If ($TrueLastLogon -ne 0){$TrueLastLogon = $TrueLastLogon.AddHours($UTCSkew)} | |
183 | ||
184 | #If TrueLastLogon is older than 20 years (essentially null/zero), set to true zero | |
185 | If ((New-TimeSpan -Start $TrueLastLogon -End $StartTime).Days -gt 7300){[string]$TrueLastLogon = $null} | |
186 | ||
187 | #Calculate days of inactivity. | |
188 | If ($TrueLastLogon -ne $null -and $TrueLastLogon -ne ""){$DaysInactive = (New-TimeSpan -Start $TrueLastLogon -End $StartTime).Days} | |
189 | Else {$DaysInactive = (New-TimeSpan -Start $whenCreated -End $StartTime).Days} | |
190 | ||
191 | #Create object for output array | |
192 | $OutputObj = New-Object -TypeName PSObject | |
193 | $OutputObj | Add-Member -MemberType NoteProperty -Name SamAccountName -Value $UserEntries[0].SamAccountName | |
194 | $OutputObj | Add-Member -MemberType NoteProperty -Name Enabled -Value $UserEntries[0].Enabled | |
195 | $OutputObj | Add-Member -MemberType NoteProperty -Name LastLogon -Value $TrueLastLogon | |
196 | $OutputObj | Add-Member -MemberType NoteProperty -Name whenCreated -Value $whenCreated | |
197 | $OutputObj | Add-Member -MemberType NoteProperty -Name DaysInactive -Value $DaysInactive | |
198 | $OutputObj | Add-Member -MemberType NoteProperty -Name GivenName -Value $UserEntries[0].GivenName | |
199 | $OutputObj | Add-Member -MemberType NoteProperty -Name Surname -Value $UserEntries[0].SurName | |
200 | $OutputObj | Add-Member -MemberType NoteProperty -Name Name -Value $UserEntries[0].Name | |
201 | $OutputObj | Add-Member -MemberType NoteProperty -Name DistinguishedName -Value $UserEntries[0].DistinguishedName | |
202 | $OutputObj | Add-Member -MemberType NoteProperty -Name Description -Value $UserEntries[0].Description | |
203 | ||
204 | #Append object to output array and output preogress to console. | |
205 | $FullResults[$i] = $OutputObj | |
206 | $PercentComplete = [math]::Round((($i/$UserCount) * 100),2) | |
207 | Write-Output ("User: " + $OutputObj.SamAccountName + " - Last logon: $TrueLastLogon ($DaysInactive day(s) inactivity) - $PercentComplete% complete.") | |
208 | } | |
209 | ||
210 | Write-Output "Getting exclusion group members..." | |
211 | $UserExclusions = (Get-ADGroupMember -Identity $ExclusionsGroup).SamAccountName | |
212 | ||
213 | #Splits "other" and "real" users into two different arrays. | |
214 | Write-Output "Filtering users..." | |
215 | $RealUsersResults = @() | |
216 | $RealUsersResults = $FullResults | Where {$UserExclusions -notcontains $_.SamAccountName} | |
217 | ||
218 | $OtherUsersResults = @() | |
219 | $FullResults = $FullResults | Where {$_ -ne $null} | |
220 | ||
221 | #For some reason compare-object is not working properly without specifying all properties. Don't know why. | |
222 | $OtherUsersResults = Compare-Object $RealUsersResults $FullResults ` | |
223 | -Property SamAccountName,enabled,lastlogon,whencreated,DaysInactive,givenname,surname,name,distinguishedname,Description | ` | |
224 | select SamAccountName,enabled,lastlogon,whencreated,DaysInactive,givenname,surname,name,distinguishedname,Description | |
225 | ||
226 | #Disable accounts with inactivity past days threshold, add to UsersDisabled array for CSV report | |
227 | $UsersDisabled = @() | |
228 | $RealUsersResults | % { | |
229 | If ($_.DaysInactive -ge $DaysThreshold){ | |
230 | Write-Output ("Disabling " + $_.SamAccountName + "...") | |
231 | Disable-ADAccount -Identity $_.SamAccountName | |
232 | $Date = "INACTIVE SINCE " + $_.LastLogon | |
233 | Set-ADUser -Identity $_.SamAccountName -Replace @{ExtensionAttribute3=$Date} | |
234 | $UsersDisabled += $_ | |
235 | } | |
236 | } | |
237 | ||
238 | #Filtered users - add to UsersNotDisabled array for CSV report | |
239 | $OtherInactiveUsers = @() | |
240 | $OtherUsersResults | % { | |
241 | If ($_.DaysInactive -ge $DaysThreshold){ | |
242 | $OtherInactiveUsers += $_ | |
243 | } | |
244 | } | |
245 | ||
246 | #Reports "Auto-Disable Exclusions-ENT" members | |
247 | $ExcludedUsersReport = $FullResults | Where {$UserExclusions -contains $_.SamAccountName} | Select * -ExcludeProperty Enabled,LastLogon,whenCreated,DaysInactive | |
248 | ||
249 | #Export CSVs to logging directory | |
250 | $UsersDisabledCSV = $LogDirectory + "\InactiveUsers-Disabled.csv" | |
251 | $UsersNotDisabledCSV = $LogDirectory + "\InactiveUsers-Excluded.csv" | |
252 | $ExcludedUsersReportCSV = $LogDirectory + "\Auto-Disable Exclusions.csv" | |
253 | $UsersDisabled | Export-CSV $UsersDisabledCSV -NoTypeInformation -Force | |
254 | $OtherInactiveUsers | Export-CSV $UsersNotDisabledCSV -NoTypeInformation -Force | |
255 | $ExcludedUsersReport | Export-CSV $ExcludedUsersReportCSV -NoTypeInformation -Force | |
256 | ||
257 | <# | |
258 | Write-Output "Starting Move-Disabled task..." | |
259 | Start-ScheduledTask -TaskName "\Move-Disabled" | |
260 | #> | |
261 | ||
262 | #Send email with CSVs as attachments | |
263 | Write-Output "Sending email..." | |
264 | Send-MailMessage -Attachments @($UsersDisabledCSV,$UsersNotDisabledCSV,$ExcludedUsersReportCSV)` | |
265 | -From $From -SmtpServer $SMTPServer -To $To -Subject $Subject | |
266 | ||
267 | Stop-Transcript |