├── .vscode
└── launch.json
├── ADHealthCheck.ps1
├── Disable-InactiveADAccounts.ps1
├── Disable-InactiveADComputers.ps1
├── Discover-DriveSpace.ps1
├── Discover-Shares.ps1
├── Dump-GPOs.ps1
├── Enumerate-Access.ps1
├── Move-Disabled.ps1
├── Move-StaleUserFolders.ps1
├── README.md
├── Restart-DFSRAndEnableAutoRecovery.ps1
└── Send-PasswordNotices.ps1
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "PowerShell",
9 | "request": "launch",
10 | "name": "PowerShell Launch Current File",
11 | "script": "${file}",
12 | "args": [],
13 | "cwd": "${file}"
14 | },
15 | {
16 | "type": "PowerShell",
17 | "request": "launch",
18 | "name": "PowerShell Launch Current File in Temporary Console",
19 | "script": "${file}",
20 | "args": [],
21 | "cwd": "${file}",
22 | "createTemporaryIntegratedConsole": true
23 | },
24 | {
25 | "type": "PowerShell",
26 | "request": "launch",
27 | "name": "PowerShell Launch Current File w/Args Prompt",
28 | "script": "${file}",
29 | "args": [
30 | "${command:SpecifyScriptArgs}"
31 | ],
32 | "cwd": "${file}"
33 | },
34 | {
35 | "type": "PowerShell",
36 | "request": "attach",
37 | "name": "PowerShell Attach to Host Process",
38 | "processId": "${command:PickPSHostProcess}",
39 | "runspaceId": 1
40 | },
41 | {
42 | "type": "PowerShell",
43 | "request": "launch",
44 | "name": "PowerShell Interactive Session",
45 | "cwd": "${workspaceRoot}"
46 | }
47 | ]
48 | }
--------------------------------------------------------------------------------
/ADHealthCheck.ps1:
--------------------------------------------------------------------------------
1 | #EDITED TO ONLY SEND EMAIL ON FAILURES/WARNINGS TO HELP CUT DOWN ON SPAM
2 | Function Start-Logging {
3 | <#
4 | .SYNOPSIS
5 | This function starts a transcript in the specified directory and cleans up any files older than the specified number of days.
6 |
7 | .DESCRIPTION
8 | Please ensure that the log directory specified is empty, as this function will clean that folder.
9 |
10 | .EXAMPLE
11 | Start-Logging -LogDirectory "C:\ScriptLogs\LogFolder" -LogName $LogName -LogRetentionDays 30
12 |
13 | .LINK
14 | https://github.com/AndrewEllis93/PowerShell-Scripts
15 |
16 | .NOTES
17 | Author: Andrew Ellis
18 | #>
19 | Param (
20 | [Parameter(Mandatory=$true)]
21 | [String]$LogDirectory,
22 | [Parameter(Mandatory=$true)]
23 | [String]$LogName,
24 | [Parameter(Mandatory=$true)]
25 | [Int]$LogRetentionDays
26 | )
27 |
28 | #Sets screen buffer from 120 width to 500 width. This stops truncation in the log.
29 | $ErrorActionPreference = 'SilentlyContinue'
30 | $pshost = Get-Host
31 | $pswindow = $pshost.UI.RawUI
32 |
33 | $newsize = $pswindow.BufferSize
34 | $newsize.Height = 3000
35 | $newsize.Width = 500
36 | $pswindow.BufferSize = $newsize
37 |
38 | $newsize = $pswindow.WindowSize
39 | $newsize.Height = 50
40 | $newsize.Width = 500
41 | $pswindow.WindowSize = $newsize
42 | $ErrorActionPreference = 'Continue'
43 |
44 | #Remove the trailing slash if present.
45 | If ($LogDirectory -like "*\") {
46 | $LogDirectory = $LogDirectory.SubString(0,($LogDirectory.Length-1))
47 | }
48 |
49 | #Create log directory if it does not exist already
50 | If (!(Test-Path $LogDirectory)) {
51 | New-Item -ItemType Directory $LogDirectory -Force | Out-Null
52 | }
53 |
54 | $Today = Get-Date -Format M-d-y
55 | Start-Transcript -Append -Path ($LogDirectory + "\" + $LogName + "." + $Today + ".log") | Out-Null
56 |
57 | #Shows proper date in log.
58 | Write-Output ("Start time: " + (Get-Date))
59 |
60 | #Purges log files older than X days
61 | $RetentionDate = (Get-Date).AddDays(-$LogRetentionDays)
62 | Get-ChildItem -Path $LogDirectory -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $RetentionDate -and $_.Name -like "*.log"} | Remove-Item -Force
63 | }
64 |
65 | #Start logging
66 | Start-Logging -LogDirectory "C:\ScriptLogs\ADHealthCheck" -LogName "ADHealthCheckLog" -LogRetentionDays 30
67 |
68 | #############################################################################
69 | # Author: Vikas Sukhija
70 | # Reviewer:
71 | # Date: 12/25/2014
72 | # Satus: Ping,Netlogon,NTDS,DNS,DCdiag Test(Replication,sysvol,Services)
73 | # Update: Added Advertising
74 | # Description: AD Health Status
75 | #############################################################################
76 | ###########################Define Variables##################################
77 |
78 | $reportpath = "C:\ScriptLogs\ADHealthCheck\ADReport.htm"
79 |
80 | if((test-path $reportpath) -like $false)
81 | {
82 | new-item $reportpath -type file
83 | }
84 | $smtphost = "smtpserver.domain.local"
85 | $from = "noreply@domain.com"
86 | $email1 = "recipient@domain.com"
87 | $timeout = "60"
88 |
89 | ###############################HTml Report Content############################
90 | $report = $reportpath
91 |
92 | Clear-Content $report
93 | Add-Content $report ""
94 | Add-Content $report "
"
95 | Add-Content $report ""
96 | Add-Content $report 'AD Status Report'
97 | add-content $report '"
122 | Add-Content $report ""
123 | Add-Content $report ""
124 | add-content $report ""
125 | add-content $report ""
126 | add-content $report ""
127 | add-content $report "Active Directory Health Check"
128 | add-content $report " | "
129 | add-content $report "
"
130 | add-content $report "
"
131 |
132 | add-content $report ""
133 | Add-Content $report ""
134 | Add-Content $report "Identity | "
135 | Add-Content $report "PingStatus | "
136 | Add-Content $report "NetlogonService | "
137 | Add-Content $report "NTDSService | "
138 | Add-Content $report "DNSServiceStatus | "
139 | Add-Content $report "NetlogonsTest | "
140 | Add-Content $report "ReplicationTest | "
141 | Add-Content $report "ServicesTest | "
142 | Add-Content $report "AdvertisingTest | "
143 | Add-Content $report "FSMOCheckTest | "
144 | Add-Content $report "DfsrLastRepTest | "
145 |
146 | Add-Content $report "
"
147 |
148 | #####################################Custom Functions#################################
149 | # Additional functions added to Vika's script for my customizations.
150 | $DeclareFunctions = {
151 | Function Get-DfsrLastUpdateTime {
152 | param ([string]$ComputerName)
153 | $ErrorActionPreference = "Stop"
154 |
155 | If (!$ComputerName){Throw "You must supply a value for ComputerName."}
156 |
157 | $DfsrWmiObj = Get-WmiObject -Namespace "root\microsoftdfs" -Class dfsrVolumeConfig -ComputerName $ComputerName
158 | If ($DfsrWmiObj.LastChangeTime.Count -le 1){
159 | [datetime]$LastChangeTime = [System.Management.ManagementDateTimeconverter]::ToDateTime($DfsrWmiObj.LastChangeTime)
160 | }
161 | Else {
162 | $OldestChangeTime = ($DfsrWmiObj.LastChangeTime | Measure-Object -Minimum).Minimum
163 | [datetime]$LastChangeTime = [System.Management.ManagementDateTimeconverter]::ToDateTime($OldestChangeTime)
164 | }
165 |
166 | Return $LastChangeTime
167 | }
168 |
169 | #This one is unused
170 | Function Get-DfsrGuid {
171 | param ([string]$ComputerName)
172 | $ErrorActionPreference = "Stop"
173 |
174 | If (!$ComputerName){Throw "You must supply a value for ComputerName."}
175 |
176 | $DfsrWmiObj = Get-WmiObject -Namespace "root\microsoftdfs" -Class dfsrVolumeConfig -ComputerName $ComputerName
177 |
178 | Return $DfsrWmiObj.VolumeGUID
179 | }
180 |
181 | Function Get-DfsrLastUpdateDelta {
182 | param ([string]$ComputerName)
183 | $ErrorActionPreference = "Stop"
184 |
185 | If (!$ComputerName){Throw "You must supply a value for ComputerName."}
186 |
187 | $LastUpdateTime = Get-DfsrLastUpdateTime -ComputerName $ComputerName
188 | $TimeDelta = (Get-Date) - $LastUpdateTime
189 |
190 | Return $TimeDelta
191 | }
192 | }
193 |
194 | #####################################Get ALL DC Servers#################################
195 | $getForest = [system.directoryservices.activedirectory.Forest]::GetCurrentForest()
196 |
197 | $DCServers = $getForest.domains | ForEach-Object {$_.DomainControllers} | ForEach-Object {$_.Name}
198 |
199 |
200 | ################Ping Test######
201 |
202 | foreach ($DC in $DCServers){
203 | $Identity = $DC
204 | Add-Content $report ""
205 | if ( Test-Connection -ComputerName $DC -Count 1 -ErrorAction SilentlyContinue ) {
206 | Write-Host $DC `t $DC `t Ping Success -ForegroundColor Green
207 |
208 | $ShortIdentity = $Identity.Replace(('.'+$getForest.Name),'')
209 | Add-Content $report "$ShortIdentity | "
210 | Add-Content $report " Success | "
211 |
212 |
213 | ##############Netlogon Service Status################
214 | $serviceStatus = start-job -scriptblock {get-service -ComputerName $($args[0]) -Name "Netlogon" -ErrorAction SilentlyContinue} -ArgumentList $DC
215 | wait-job $serviceStatus -timeout $timeout
216 | if($serviceStatus.state -like "Running")
217 | {
218 | Write-Host $DC `t Netlogon Service TimeOut -ForegroundColor Yellow
219 | Add-Content $report "Timeout | "
220 | stop-job $serviceStatus
221 | }
222 | else
223 | {
224 | $serviceStatus1 = Receive-job $serviceStatus
225 | if ($serviceStatus1.status -eq "Running") {
226 | Write-Host $DC `t $serviceStatus1.name `t $serviceStatus1.status -ForegroundColor Green
227 | $svcName = $serviceStatus1.name
228 | $svcState = $serviceStatus1.status
229 | Add-Content $report "$svcState | "
230 | }
231 | else
232 | {
233 | Write-Host $DC `t $serviceStatus1.name `t $serviceStatus1.status -ForegroundColor Red
234 | $svcName = $serviceStatus1.name
235 | $svcState = $serviceStatus1.status
236 | Add-Content $report "$svcState | "
237 | }
238 | }
239 | ######################################################
240 | ##############NTDS Service Status################
241 | $serviceStatus = start-job -scriptblock {get-service -ComputerName $($args[0]) -Name "NTDS" -ErrorAction SilentlyContinue} -ArgumentList $DC
242 | wait-job $serviceStatus -timeout $timeout
243 | if($serviceStatus.state -like "Running")
244 | {
245 | Write-Host $DC `t NTDS Service TimeOut -ForegroundColor Yellow
246 | Add-Content $report "Timeout | "
247 | stop-job $serviceStatus
248 | }
249 | else
250 | {
251 | $serviceStatus1 = Receive-job $serviceStatus
252 | if ($serviceStatus1.status -eq "Running") {
253 | Write-Host $DC `t $serviceStatus1.name `t $serviceStatus1.status -ForegroundColor Green
254 | $svcName = $serviceStatus1.name
255 | $svcState = $serviceStatus1.status
256 | Add-Content $report "$svcState | "
257 | }
258 | else
259 | {
260 | Write-Host $DC `t $serviceStatus1.name `t $serviceStatus1.status -ForegroundColor Red
261 | $svcName = $serviceStatus1.name
262 | $svcState = $serviceStatus1.status
263 | Add-Content $report "$svcState | "
264 | }
265 | }
266 | ######################################################
267 | ##############DNS Service Status################
268 | $serviceStatus = start-job -scriptblock {get-service -ComputerName $($args[0]) -Name "DNS" -ErrorAction SilentlyContinue} -ArgumentList $DC
269 | wait-job $serviceStatus -timeout $timeout
270 | if($serviceStatus.state -like "Running")
271 | {
272 | Write-Host $DC `t DNS Server Service TimeOut -ForegroundColor Yellow
273 | Add-Content $report "Timeout | "
274 | stop-job $serviceStatus
275 | }
276 | else
277 | {
278 | $serviceStatus1 = Receive-job $serviceStatus
279 | if ($serviceStatus1.status -eq "Running") {
280 | Write-Host $DC `t $serviceStatus1.name `t $serviceStatus1.status -ForegroundColor Green
281 | $svcName = $serviceStatus1.name
282 | $svcState = $serviceStatus1.status
283 | Add-Content $report "$svcState | "
284 | }
285 | else
286 | {
287 | Write-Host $DC `t $serviceStatus1.name `t $serviceStatus1.status -ForegroundColor Red
288 | $svcName = $serviceStatus1.name
289 | $svcState = $serviceStatus1.status
290 | Add-Content $report "$svcState | "
291 | }
292 | }
293 | ######################################################
294 |
295 | ####################Netlogons status##################
296 | add-type -AssemblyName microsoft.visualbasic
297 | $cmp = "microsoft.visualbasic.strings" -as [type]
298 | $sysvol = start-job -scriptblock {dcdiag /test:netlogons /s:$($args[0])} -ArgumentList $DC
299 | wait-job $sysvol -timeout $timeout
300 | if($sysvol.state -like "Running")
301 | {
302 | Write-Host $DC `t Netlogons Test TimeOut -ForegroundColor Yellow
303 | Add-Content $report "Timeout | "
304 | stop-job $sysvol
305 | }
306 | else
307 | {
308 | $sysvol1 = Receive-job $sysvol
309 | if($cmp::instr($sysvol1, "passed test NetLogons"))
310 | {
311 | Write-Host $DC `t Netlogons Test passed -ForegroundColor Green
312 | Add-Content $report "Pass | "
313 | }
314 | else
315 | {
316 | Write-Host $DC `t Netlogons Test Failed -ForegroundColor Red
317 | Add-Content $report "Fail | "
318 | }
319 | }
320 | ########################################################
321 | ####################Replications status##################
322 | add-type -AssemblyName microsoft.visualbasic
323 | $cmp = "microsoft.visualbasic.strings" -as [type]
324 | $sysvol = start-job -scriptblock {dcdiag /test:Replications /s:$($args[0])} -ArgumentList $DC
325 | wait-job $sysvol -timeout $timeout
326 | if($sysvol.state -like "Running")
327 | {
328 | Write-Host $DC `t Replications Test TimeOut -ForegroundColor Yellow
329 | Add-Content $report "Timeout | "
330 | stop-job $sysvol
331 | }
332 | else
333 | {
334 | $sysvol1 = Receive-job $sysvol
335 | if($cmp::instr($sysvol1, "passed test Replications"))
336 | {
337 | Write-Host $DC `t Replications Test passed -ForegroundColor Green
338 | Add-Content $report "Pass | "
339 | }
340 | else
341 | {
342 | Write-Host $DC `t Replications Test Failed -ForegroundColor Red
343 | Add-Content $report "Fail | "
344 | }
345 | }
346 | ########################################################
347 | ####################Services status##################
348 | add-type -AssemblyName microsoft.visualbasic
349 | $cmp = "microsoft.visualbasic.strings" -as [type]
350 | $sysvol = start-job -scriptblock {dcdiag /test:Services /s:$($args[0])} -ArgumentList $DC
351 | wait-job $sysvol -timeout $timeout
352 | if($sysvol.state -like "Running")
353 | {
354 | Write-Host $DC `t Services Test TimeOut -ForegroundColor Yellow
355 | Add-Content $report "Timeout | "
356 | stop-job $sysvol
357 | }
358 | else
359 | {
360 | $sysvol1 = Receive-job $sysvol
361 | if($cmp::instr($sysvol1, "passed test Services"))
362 | {
363 | Write-Host $DC `t Services Test passed -ForegroundColor Green
364 | Add-Content $report "Pass | "
365 | }
366 | else
367 | {
368 | Write-Host $DC `t Services Test Failed -ForegroundColor Red
369 | Add-Content $report "Fail | "
370 | }
371 | }
372 | ########################################################
373 | ####################Advertising status##################
374 | add-type -AssemblyName microsoft.visualbasic
375 | $cmp = "microsoft.visualbasic.strings" -as [type]
376 | $sysvol = start-job -scriptblock {dcdiag /test:Advertising /s:$($args[0])} -ArgumentList $DC
377 | wait-job $sysvol -timeout $timeout
378 | if($sysvol.state -like "Running")
379 | {
380 | Write-Host $DC `t Advertising Test TimeOut -ForegroundColor Yellow
381 | Add-Content $report "Timeout | "
382 | stop-job $sysvol
383 | }
384 | else
385 | {
386 | $sysvol1 = Receive-job $sysvol
387 | if($cmp::instr($sysvol1, "passed test Advertising"))
388 | {
389 | Write-Host $DC `t Advertising Test passed -ForegroundColor Green
390 | Add-Content $report "Pass | "
391 | }
392 | else
393 | {
394 | Write-Host $DC `t Advertising Test Failed -ForegroundColor Red
395 | Add-Content $report "Fail | "
396 | }
397 | }
398 | ########################################################
399 | ####################FSMOCheck status##################
400 | add-type -AssemblyName microsoft.visualbasic
401 | $cmp = "microsoft.visualbasic.strings" -as [type]
402 | $sysvol = start-job -scriptblock {dcdiag /test:FSMOCheck /s:$($args[0])} -ArgumentList $DC
403 | wait-job $sysvol -timeout $timeout
404 | if($sysvol.state -like "Running")
405 | {
406 | Write-Host $DC `t FSMOCheck Test TimeOut -ForegroundColor Yellow
407 | Add-Content $report "Timeout | "
408 | stop-job $sysvol
409 | }
410 | else
411 | {
412 | $sysvol1 = Receive-job $sysvol
413 | if($cmp::instr($sysvol1, "passed test FsmoCheck"))
414 | {
415 | Write-Host $DC `t FSMOCheck Test passed -ForegroundColor Green
416 | Add-Content $report "Pass | "
417 | }
418 | else
419 | {
420 | Write-Host $DC `t FSMOCheck Test Failed -ForegroundColor Red
421 | Add-Content $report "Fail | "
422 | }
423 | }
424 | ########################################################
425 | ####################DfsrRep status##################
426 | # Additional column added to Vika's script for my customizations.
427 | $DfsrLastUpdateJob = start-job -InitializationScript $DeclareFunctions -scriptblock {Get-DFSRLastUpdateDelta -ComputerName $args[0]} -ArgumentList $DC
428 | wait-job $DfsrLastUpdateJob -timeout $timeout
429 |
430 | if($DfsrLastUpdateJob.state -like "Running"){
431 | Write-Host $DC `t DFSR Last Rep Test TimeOut -ForegroundColor Yellow
432 | Add-Content $report "Timeout | "
433 | stop-job $DfsrLastUpdateJob
434 | }
435 | else{
436 | $DfsrLastUpdateDelta = Receive-job $DfsrLastUpdateJob
437 | If ($DfsrLastUpdateJob.state -eq "Failed"){$DfsrLastUpdateTestResults = "Fail (Unreadable)"}
438 | ElseIf ($DfsrLastUpdateDelta.Hours -ge 23){$DfsrLastUpdateTestResults = ("Fail (" + $DfsrLastUpdateDelta.Minutes + " Min)")}
439 | Else {$DfsrLastUpdateTestResults = ("Pass (" + $DfsrLastUpdateDelta.Minutes + " Min)")}
440 |
441 | if($DfsrLastUpdateTestResults -notlike "Fail*") {
442 | Write-Host $DC `t DFSR Last Rep Test passed -ForegroundColor Green
443 | Add-Content $report "$DfsrLastUpdateTestResults | "
444 | }
445 | else {
446 | Write-Host $DC `t DFSR Last Rep Test Failed -ForegroundColor Red
447 | Add-Content $report "$DfsrLastUpdateTestResults | "
448 | }
449 | }
450 | }
451 | else {
452 | Write-Host $DC `t $DC `t Ping Fail -ForegroundColor Red
453 | Add-Content $report " $Identity | "
454 | Add-Content $report " Ping Fail | "
455 | Add-Content $report " Ping Fail | "
456 | Add-Content $report " Ping Fail | "
457 | Add-Content $report " Ping Fail | "
458 | Add-Content $report " Ping Fail | "
459 | Add-Content $report " Ping Fail | "
460 | Add-Content $report " Ping Fail | "
461 | Add-Content $report " Ping Fail | "
462 | Add-Content $report " Ping Fail | "
463 | }
464 | }
465 |
466 | Add-Content $report "
"
467 | ############################################Close HTMl Tables###########################
468 |
469 |
470 | Add-content $report "
"
471 | Add-Content $report ""
472 | Add-Content $report ""
473 |
474 |
475 | ########################################################################################
476 | #############################################Send Email#################################
477 | $IsHealthy = Get-Content $reportpath | Select-String -Pattern "Fail|Stopped|Timeout"
478 | If ($IsHealthy -ne $null)
479 | {
480 | $subject = "Daily AD Health Check - UNHEALTHY"
481 | $body = Get-Content $reportpath
482 | $smtp= New-Object System.Net.Mail.SmtpClient $smtphost
483 | $msg = New-Object System.Net.Mail.MailMessage
484 | $msg.To.Add($email1)
485 | $msg.from = $from
486 | $msg.subject = $subject
487 | $msg.body = $body
488 | $msg.isBodyhtml = $true
489 | $smtp.send($msg)
490 | }
491 |
492 | ########################################################################################
493 |
494 | ########################################################################################
495 |
496 | Stop-Transcript
--------------------------------------------------------------------------------
/Disable-InactiveADAccounts.ps1:
--------------------------------------------------------------------------------
1 | ####################################################
2 | #
3 | # Title: Disable-InactiveADAccounts
4 | # Date Created : 2017-09-22
5 | # Last Edit: 2018-02-14
6 | # Author : Andrew Ellis
7 | # GitHub: https://github.com/AndrewEllis93/PowerShell-Scripts
8 | #
9 | # Notes:
10 | # This finds the last logon for all AD accounts and disables any that have been inactive for X number of days (depending on what threshold you set).
11 | # The difference with this script is that it gets the most accurate last logon available by comparing the results from all domain controllers. By default the lastlogontimestamp is only replicated every 14 days minus a random percentage of 5. This makes it much more accurate, but also it is a very slow. It also supports an exclusion AD group that you can put things like service accounts in to prevent them from being disabled. It will also email a report to the specified email addresses.
12 | # WARNING: THIS SCRIPT WILL OVERWRITE EXTENSIONATTRIBUTE3 FOR ALL USERS, MAKE SURE YOU ARE NOT USING IT FOR ANYTHING ELSE
13 | # 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.
14 | #
15 | ####################################################
16 |
17 | #Function declarations
18 | Function Start-Logging {
19 | <#
20 | .SYNOPSIS
21 | This function starts a transcript in the specified directory and cleans up any files older than the specified number of days.
22 |
23 | .DESCRIPTION
24 | Please ensure that the log directory specified is empty, as this function will clean that folder.
25 |
26 | .EXAMPLE
27 | Start-Logging -LogDirectory "C:\ScriptLogs\LogFolder" -LogName $LogName -LogRetentionDays 30
28 |
29 | .LINK
30 | https://github.com/AndrewEllis93/PowerShell-Scripts
31 |
32 | .NOTES
33 | Author: Andrew Ellis
34 | #>
35 | Param (
36 | [Parameter(Mandatory=$true)]
37 | [String]$LogDirectory,
38 | [Parameter(Mandatory=$true)]
39 | [String]$LogName,
40 | [Parameter(Mandatory=$true)]
41 | [Int]$LogRetentionDays
42 | )
43 |
44 | #Sets screen buffer from 120 width to 500 width. This stops truncation in the log.
45 | $ErrorActionPreference = 'SilentlyContinue'
46 | $pshost = Get-Host
47 | $pswindow = $pshost.UI.RawUI
48 |
49 | $newsize = $pswindow.BufferSize
50 | $newsize.Height = 3000
51 | $newsize.Width = 500
52 | $pswindow.BufferSize = $newsize
53 |
54 | $newsize = $pswindow.WindowSize
55 | $newsize.Height = 50
56 | $newsize.Width = 500
57 | $pswindow.WindowSize = $newsize
58 | $ErrorActionPreference = 'Continue'
59 |
60 | #Remove the trailing slash if present.
61 | If ($LogDirectory -like "*\") {
62 | $LogDirectory = $LogDirectory.SubString(0,($LogDirectory.Length-1))
63 | }
64 |
65 | #Create log directory if it does not exist already
66 | If (!(Test-Path $LogDirectory)) {
67 | New-Item -ItemType Directory $LogDirectory -Force | Out-Null
68 | }
69 |
70 | $Today = Get-Date -Format M-d-y
71 | Start-Transcript -Append -Path ($LogDirectory + "\" + $LogName + "." + $Today + ".log") | Out-Null
72 |
73 | #Shows proper date in log.
74 | Write-Output ("Start time: " + (Get-Date))
75 |
76 | #Purges log files older than X days
77 | $RetentionDate = (Get-Date).AddDays(-$LogRetentionDays)
78 | Get-ChildItem -Path $LogDirectory -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $RetentionDate -and $_.Name -like "*.log"} | Remove-Item -Force
79 | }
80 | Function Disable-InactiveADAccounts {
81 | <#
82 | .SYNOPSIS
83 | This script disables AD accounts older than the threshold (in days) and stamps them in ExtensionAttribute3 with the disabled date. It also sends an email report.
84 |
85 | .DESCRIPTION
86 | Make sure you read through the comments (as with all of these scripts). It just finds the last logon for all AD accounts and disables any that have been inactive for X number of days (depending on what threshold you set). The difference with this script is that it gets the most accurate last logon available by comparing the results from all domain controllers. By default the lastlogontimestamp is only replicated every 14 days minus a random percentage of 5. This makes it much more accurate. It also supports an exclusion AD group that you can put things like service accounts in to prevent them from being disabled. It will also email a report to the specified email addresses.
87 | "-ReportOnly" will skip actually disabling the AD accounts and just send an email report of inactivity instead.
88 |
89 | .EXAMPLE
90 | Disable-InactiveADAccounts -To @("email@domain.com","email2@domain.com") -From "noreply@domain.com" -SMTPServer "server.domain.local" -UTCSkew -5 -OutputDirectory "C:\ScriptLogs\Disable-InactiveADAccounts" -ExclusionGroup @('ServiceAccounts','Auto-Disable Exclusions') -DaysThreshold 30 -ReportOnly $True
91 |
92 | .LINK
93 | https://github.com/AndrewEllis93/PowerShell-Scripts
94 |
95 | .NOTES
96 | Author: Andrew Ellis
97 | #>
98 |
99 | Param(
100 | #From address for email reports.
101 | [Parameter(Mandatory=$true)]
102 | [String]$From,
103 |
104 | #If $true, email report will be sent without disabling or stamping any AD accounts.
105 | [switch]$ReportOnly = $False,
106 |
107 | #SMTP server for sending reports.
108 | [Parameter(Mandatory=$true)]
109 | [String]$SMTPServer,
110 |
111 | #Array. You can add more than one entry.
112 | [Parameter(Mandatory=$true)]
113 | [Array]$To,
114 |
115 | #Accounting for the time zone difference, since some results are given in UTC. Eastern time is UTC-5.
116 | [Parameter(Mandatory=$true)]
117 | [Int]$UTCSkew,
118 |
119 | #Threshold of days of inactivity before disabling the user. Defaults to 30 days.
120 | [Int]$DaysThreshold = 30,
121 |
122 | #Where to export CSVs etc.
123 | [Parameter(Mandatory=$true)]
124 | [String]$OutputDirectory,
125 |
126 | #Subject for email reports.
127 | [String]$Subject = "Account Cleanup Report",
128 |
129 | #Amount of times to try for identical DC results before giving up. 30 second retry delay after each failure.
130 | [Int]$MaxTryCount = 20,
131 |
132 | #AD group containing accounts to exclude.
133 | [array]$ExclusionGroups
134 | )
135 |
136 | #Remove trailing slash if present.
137 | If ($OutputDirectory -like "*\") {
138 | $OutputDirectory = $OutputDirectory.substring(0,($OutputDirectory.Length-1))
139 | }
140 |
141 | #RE-ENABLED ACCOUNT FLAGGING
142 | #Gets all AD objects with ExtensionAttribute3 set to an inactivity or disablement date and sets it to "RE-ENABLED ON "
143 | Write-Output ""
144 | Write-Output "RE-ENABLED ACCOUNT FLAGGING:"
145 | Write-Output "Finding unflagged re-enabled users..."
146 |
147 | $ReEnabledUsers = Get-ADUser -Filter * -Properties DistinguishedName,ExtensionAttribute3,SamAccountName,Enabled | Where-Object {
148 | $_.Enabled -eq $True -and
149 | (
150 | $_.ExtensionAttribute3 -like "DISABLED ON*" -or
151 | $_.ExtensionAttribute3 -like "INACTIVE SINCE*"
152 | )
153 | }
154 |
155 | Write-Output (($ReEnabledUsers.SamAccountName.Count).ToString() + " users were found.")
156 |
157 | $Date = "RE-ENABLED ON " + (Get-Date)
158 |
159 | ForEach ($ReEnabledUser in $ReEnabledUsers){
160 | Write-Output ("Setting ExtensionAttribute3 re-enabled flag for " + $ReEnabledUser.SamAccountName + "...")
161 | If ($ReportOnly){
162 | Set-ADUser -Identity $ReEnabledUser.SamAccountName -Replace @{ExtensionAttribute3=$Date} -WhatIf
163 | }
164 | Else {
165 | Set-ADUser -Identity $ReEnabledUser.SamAccountName -Replace @{ExtensionAttribute3=$Date}
166 | }
167 | }
168 |
169 | #OLD RE-ENABLED ACCOUNT CLEANUP
170 | #Gets all users with ExtensionAttribute3 set to an expired re-enable date to clear it.
171 | Write-Output ""
172 | Write-Output 'CLEANUP - EXPIRED "RE-ENABLED" FLAGS:'
173 | Write-Output 'Finding users with expired "re-enabled" flags...'
174 |
175 | $ExpiredFlagUsers = Get-ADUser -Filter * -Properties DistinguishedName,ExtensionAttribute3,SamAccountName,Enabled | Where-Object {
176 | $_.ExtensionAttribute3 -like "RE-ENABLED*" -and
177 | [datetime]($_.ExtensionAttribute3 -replace 'RE-ENABLED ON ', '') -lt (Get-Date).AddDays(-$DaysThreshold)
178 | }
179 |
180 | Write-Output (($ExpiredFlagUsers.SamAccountName.Count).ToString() + " users were found.")
181 |
182 | ForEach ($ExpiredFlagUser in $ExpiredFlagUsers){
183 | Write-Output ("Clearing ExtensionAttribute3 re-enabled flag for " + $ExpiredFlagUser.SamAccountName + "...")
184 | If ($ReportOnly){
185 | Set-ADUser -Identity $ExpiredFlagUser.SamAccountName -Clear ExtensionAttribute3 -WhatIf
186 | }
187 | Else {
188 | Set-ADUser -Identity $ExpiredFlagUser.SamAccountName -Clear ExtensionAttribute3
189 | }
190 | }
191 |
192 | #INACTIVE USER DISABLEMENT AND FLAGGING
193 | #Declare try count at 0.
194 | $TryCount= 0
195 |
196 | #Get all DCs, add array names to vars array
197 | $DCnames = (Get-ADGroupMember 'Domain Controllers').Name | Sort-Object
198 |
199 | #This just tests if we already have results for each DC, in case we are running this twice in the same session (mostly just for testing).
200 | $ExistingResults = @(0) * $DCnames.Count
201 | $TestIteration = 0
202 | $DCnames | ForEach-Object {
203 | If (Get-Variable -Name $_ -ErrorAction SilentlyContinue){
204 | $ExistingResults[$TestIteration] = $True
205 | }
206 | Else {
207 | $ExistingResults[$TestIteration] = $False
208 | }
209 | $TestIteration++
210 | }
211 |
212 | #Check that results match from each DC by comparing all results in order. Retry if there is a mismatch, up to the MaxTryCount (default 20)
213 | While (($ComparisonResults -contains $False -or !$ComparisonResults) -and $TryCount -lt $MaxTryCount){
214 | #Makes sure we don't have any left over jobs from another run
215 | Get-Job | Stop-Job
216 | Get-Job | Remove-Job
217 |
218 | If ((!$ExistingResults -or $ExistingResults -contains $False) -or ($ComparisonResults -contains $False -or !$ComparisonResults)){
219 | #Fetch AD users from each DC, add to named array
220 | Write-Output ""
221 | Write-Output "Starting data retrieval jobs..."
222 |
223 | ForEach ($DCName in $DCnames) {
224 | Start-Job -Name $DCName -ArgumentList $DCName -ScriptBlock {
225 | param($DCName)
226 | #Get AD results
227 | Import-Module ActiveDirectory
228 | $Results = Get-ADUser -Filter {Enabled -eq $True} -Server $DCName -Properties DistinguishedName,LastLogon,LastLogonTimestamp,whenCreated,Description,ExtensionAttribute3 -ErrorAction Stop
229 | $Results = $Results | Sort-Object -Property SamAccountName
230 | Return $Results
231 | }
232 | }
233 |
234 | #Wait for jobs to complete, show progress bar
235 | Wait-JobsWithProgress -Activity "Retrieving and sorting results from each DC. Please be patient"
236 |
237 | #Put results into named arrays for each DC
238 | ForEach ($DCName in $DCnames) {
239 | Set-Variable -Name $DCName -Value (Receive-Job -Name $DCName)
240 | }
241 | }
242 |
243 | $ComparisonResults = @()
244 |
245 | ForEach ($i in 0..(($DCnames.Count)-1)){
246 | If ($i -le (($DCnames.Count)-2)){
247 | Write-Output ("Comparing results from " + $DCnames[$i] + " and " + $DCnames[$i+1] + "...")
248 | $NotEqual = Compare-Object (Get-Variable -Name $DCnames[$i]).Value (Get-Variable -Name $DCnames[$i+1]).Value -Property SamAccountName
249 |
250 | If (!$NotEqual) {
251 | $ComparisonResults += $True
252 | }
253 | Else {
254 | $ComparisonResults += $False
255 | }
256 | }
257 | }
258 | If ($ComparisonResults -contains $False){
259 | Write-Warning "One or more DCs returned differing results. This is likely just replication delay. Retrying..."
260 | $TryCount++
261 | }
262 | }
263 | If ($TryCount -lt $MaxTryCount){
264 | Write-Output "All DC results are identical!"
265 | }
266 | Else {
267 | Throw "Try limit exceeded. Aborting."
268 | }
269 |
270 | #Removes the completes jobs.
271 | Get-Job | Remove-Job
272 |
273 | #Convert our results into hash tables because they are MUCH faster to process than PSObjects.
274 | If (!$ExistingResults -or $ExistingResults -contains $False){
275 | Write-Output ""
276 | Write-Output "Starting hash table conversions..."
277 | Write-Output ""
278 | ForEach ($DCName in $DCnames) {
279 | [array]$Data = (Get-Variable -Name $DCName).Value
280 | $Count = (Get-Variable -Name $DCName).Value.Count
281 |
282 | Start-Job -Name $DCName -ArgumentList $Data,$Count -ScriptBlock {
283 | param(
284 | [array]$Data,
285 | $Count
286 | )
287 | #Function to convert objects to hash tables
288 | #Credit: https://gist.github.com/dlwyatt/4166704557cf73bdd3ae
289 | Function ConvertTo-Hashtable{
290 | [CmdletBinding()]
291 | Param (
292 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
293 | [psobject[]] $InputObject
294 | )
295 | Process{
296 | ForEach ($object in $InputObject){
297 | $hash = @{}
298 |
299 | ForEach ($property in $object.PSObject.Properties){
300 | $hash[$property.Name] = $property.Value
301 | }
302 |
303 | $hash
304 | }
305 | }
306 | }
307 | #Declare the results array with empty hash tables to put the hash table objects into.
308 | [array]$HashResults = @(@{}) * $Count
309 |
310 | #Loop through each object, convert to hash table, add to HashResults array.
311 | $Iteration = 0
312 | $Data | ForEach-Object {
313 | $HashResults[$Iteration] = $_ | ConvertTo-Hashtable
314 | $Iteration++
315 | }
316 | Return $HashResults
317 | }
318 | }
319 | }
320 |
321 | #Wait for jobs to complete, show progress bar
322 | Wait-JobsWithProgress -Activity "Converting results to hash tables"
323 |
324 | #Get the hash table results from the jobs.
325 | If (!$ExistingResults -or $ExistingResults -contains $False){
326 | ForEach ($DCName in $DCNames){
327 | Set-Variable -Name $DCName -Value (Receive-Job -Name $DCName) -Force
328 | }
329 | }
330 |
331 | #Get current time for comparison later.
332 | $StartTime = Get-Date
333 |
334 | #User count so we know how many times to loop.
335 | $UserCount = (Get-Variable -Name $DCnames[0]).Value.Count
336 |
337 | #Create results array of the same size
338 | $FullResults = @($null) * $UserCount
339 |
340 | #Loop through array indexes
341 | ForEach ($i in 0..($UserCount -1)){
342 | $ReEnabledDate = $null
343 |
344 | #Grab user object from each resultant array, make array of each user object
345 | $UserEntries = @(@{}) * $DCnames.Count
346 | ForEach ($o in 0..($DCnames.Count -1)) {
347 | $UserEntries[$o] = (Get-Variable -Name $DCnames[$o]).Value[$i]
348 | }
349 |
350 | #If that user's array contains a mismatch, bail. This should realistically never happen because we already compared the arrays.
351 | If (($UserEntries.SamAccountName | Select-Object -Unique).Count -gt 1){
352 | Throw "A user mismatch at index $i has occurred. Aborting."
353 | }
354 |
355 | #Find most recent LastLogon, whenCreated, and LastLogonTimestamps, cast to datetimes.
356 | If ($UserEntries.LastLogon){
357 | [datetime]$LastLogon = [datetime]::FromFileTimeUtc(($UserEntries.LastLogon | Measure-Object -Maximum).Maximum)
358 | $LastLogon = $LastLogon.AddHours($UTCSkew)
359 | [datetime]$TrueLastLogon = $LastLogon
360 | }
361 | Else {[datetime]$LastLogon = 0; $TrueLastLogon = 0}
362 |
363 | [datetime]$whenCreated = $UserEntries[0].whenCreated
364 |
365 | If ($UserEntries.LastLogonTimestamp){
366 | [datetime]$LastLogonTimestamp = [datetime]::FromFileTimeUtc(($UserEntries.LastLogonTimestamp | Measure-Object -Maximum).Maximum)
367 | $LastLogonTimestamp = $LastLogonTimestamp.AddHours($UTCSkew)
368 | }
369 | Else {[datetime]$LastLogonTimestamp = 0}
370 |
371 | #If LastLogonTimestamp is newer, use that instead of LastLogon. Realistically this should never happen, but just in case.
372 | If ($LastLogonTimestamp -gt $LastLogon){
373 | $TrueLastLogon = $LastLogonTimestamp
374 | }
375 |
376 | #If there is no last logon available from any attributes, or it is older than 20 years (essentially null/zero), use the date created instead.
377 | If ($TrueLastLogon -eq 0 -or !$TrueLastLogon -or (New-TimeSpan -Start $TrueLastLogon -End $StartTime).Days -gt 7300){
378 | [datetime]$TrueLastLogon = $whenCreated
379 | }
380 |
381 | #If the account was flagged as re-enabled, take that into consideration too.
382 | If ($UserEntries.ExtensionAttribute3 -like "RE-ENABLED ON*"){
383 | $ReEnabledDate = [datetime](($UserEntries.ExtensionAttribute3 | Measure-Object -Maximum).Maximum -replace 'RE-ENABLED ON ', '')
384 | If ($ReEnabledDate -gt $TrueLastLogon){
385 | $TrueLastLogon = $ReEnabledDate
386 | }
387 | }
388 |
389 | #Calculate days of inactivity.
390 | $DaysInactive = (New-TimeSpan -Start $TrueLastLogon -End $StartTime).Days
391 |
392 | #Create object for output array
393 | $OutputObj = [PSCustomObject]@{
394 | SamAccountName=$UserEntries[0].SamAccountName
395 | Enabled=$UserEntries[0].Enabled
396 | LastLogon=$TrueLastLogon
397 | WhenCreated=$whenCreated
398 | DaysInactive=$DaysInactive
399 | GivenName=$UserEntries[0].GivenName
400 | Surname=$UserEntries[0].SurName
401 | Name=$UserEntries[0].Name
402 | DistinguishedName=$UserEntries[0].DistinguishedName
403 | Description=$UserEntries[0].Description
404 | ReEnabledDate=$ReEnabledDate
405 | }
406 |
407 | #Append object to output array and output progress to console.
408 | $FullResults[$i] = $OutputObj
409 | $PercentComplete = [math]::Round((($i/$UserCount) * 100),2)
410 | Write-Output ("User: " + $OutputObj.SamAccountName + " - Last logon: $TrueLastLogon ($DaysInactive day(s) inactivity) - $PercentComplete% complete.")
411 | }
412 |
413 | #Gets exlusions, error action is set to stop
414 | If ($ExclusionGroups){
415 | $UserExclusions = @()
416 | ForEach ($ExclusionGroup in $ExclusionGroups){
417 | Write-Output "Getting `"$ExclusionGroup`" members..."
418 | $UserExclusions += (Get-ADGroupMember -Identity $ExclusionGroup -ErrorAction Stop).SamAccountName
419 | }
420 | }
421 |
422 | #Filter
423 | Write-Output "Filtering users..."
424 | $FilteredUsersResults = $FullResults | Where-Object {$UserExclusions -notcontains $_.SamAccountName}
425 | $FullResults = $FullResults | Where-Object {$_ -ne $null}
426 |
427 | #For some reason compare-object is not working properly without specifying all properties. Don't know why.
428 | $ExcludedUsersResults = Compare-Object $FilteredUsersResults $FullResults `
429 | -Property SamAccountName,enabled,lastlogon,whencreated,DaysInactive,givenname,surname,name,distinguishedname,Description,ExtensionAttribute3 |
430 | Select-Object SamAccountName,enabled,lastlogon,whencreated,DaysInactive,givenname,surname,name,distinguishedname,Description,ExtensionAttribute3
431 |
432 | #Add to UsersDisabled array for CSV report. Also disable and stamp accounts if ReportOnly is set to false (default).
433 | $InactiveUsersDisabled = @()
434 | If (!$ReportOnly){
435 | $FilteredUsersResults | ForEach-Object {
436 | If ($_.DaysInactive -ge $DaysThreshold){
437 | Write-Output ("Disabling " + $_.SamAccountName + "...")
438 | Disable-ADAccount -Identity $_.SamAccountName
439 | $Date = "INACTIVE SINCE " + $_.LastLogon
440 | Set-ADUser -Identity $_.SamAccountName -Replace @{ExtensionAttribute3=$Date}
441 | $InactiveUsersDisabled += $_
442 | }
443 | }
444 | }
445 | Else {
446 | $FilteredUsersResults | ForEach-Object {
447 | If ($_.DaysInactive -ge $DaysThreshold){
448 | Write-Output ("Disabling " + $_.SamAccountName + "...")
449 | Disable-ADAccount -Identity $_.SamAccountName -WhatIf
450 | $Date = "INACTIVE SINCE " + $_.LastLogon
451 | Set-ADUser -Identity $_.SamAccountName -Replace @{ExtensionAttribute3=$Date} -WhatIf
452 | $InactiveUsersDisabled += $_
453 | }
454 | }
455 | }
456 |
457 | #Filtered users - add to UsersNotDisabled array for CSV report
458 | $ExcludedInactiveUsers = @()
459 | $ExcludedUsersResults | ForEach-Object {
460 | If ($_.DaysInactive -ge $DaysThreshold){
461 | $ExcludedInactiveUsers += $_
462 | }
463 | }
464 |
465 | #Create output directory if it does not exist
466 | If (!(Test-Path $OutputDirectory)){
467 | New-Item -ItemType Directory $OutputDirectory
468 | }
469 |
470 | #Form the paths for the output files
471 | $InactiveUsersDisabledCSV = $OutputDirectory + "\InactiveUsers-Disabled.csv"
472 | $InactiveUsersExcludedCSV = $OutputDirectory + "\InactiveUsers-Excluded.csv"
473 |
474 | #Export the CSVs
475 | $InactiveUsersDisabled | Export-CSV $InactiveUsersDisabledCSV -NoTypeInformation -Force
476 | If ($ExclusionGroups){
477 | $ExcludedInactiveUsers | Export-CSV $InactiveUsersExcludedCSV -NoTypeInformation -Force
478 | }
479 |
480 | #Send email with CSVs as attachments
481 | Write-Output "Sending email..."
482 |
483 | If ($ExclusionGroups){
484 | $ExclusionGroupsList = $ExclusionGroups -join ", "
485 | $Body = @"
486 | Attached are two reports:
487 | - InactiveUsers-Disabled.csv: AD accounts that were disabled for inactivity ($DaysThreshold days).
488 | - InactiveUsers-Excluded.csv: Inactive AD accounts that were excluded from disablement.
489 |
490 | Excluded AD group(s): $ExclusionGroupsList
491 | "@
492 | }
493 | Else {
494 | $Body = "Attached is a list of AD accounts that were disabled for inactivity ($DaysThreshold days)."
495 |
496 | }
497 |
498 | If ($ExclusionGroups){
499 | Send-MailMessage -Attachments @($InactiveUsersDisabledCSV,$InactiveUsersExcludedCSV) -From $From -SmtpServer $SMTPServer -To $To -Subject $Subject -Body $Body
500 | }
501 | Else {
502 | Send-MailMessage -Attachments @($InactiveUsersDisabledCSV) -From $From -SmtpServer $SMTPServer -To $To -Subject $Subject -Body $Body
503 | }
504 |
505 | <#
506 | # This is here if you want to use it in conjunction with my Move-Disabled script. Just uncomment and replace with your scheduled task path.
507 | Write-Output "Starting Move-Disabled task..."
508 | Start-ScheduledTask -TaskName "\Move-Disabled"
509 | #>
510 | }
511 |
512 | Function Wait-JobsWithProgress {
513 | param(
514 | [Parameter(Mandatory=$true)]
515 | [string]$Activity
516 | )
517 | # SHOW JOB PROGRESS
518 | $Total = (Get-Job).Count
519 | $CompletedJobs = (Get-Job -State Completed).Count
520 |
521 | # Loop while there are running jobs
522 | While ($CompletedJobs -ne $Total) {
523 | # Update progress based on how many jobs are done yet.
524 | # Write-Output "Waiting for background jobs: $CompletedJobs/$Total"
525 | Write-Progress -Activity $Activity -PercentComplete (($CompletedJobs/$Total)*100) -Status "$CompletedJobs/$Total jobs completed"
526 |
527 | # After updating the progress bar, get current job count
528 | $CompletedJobs = (Get-Job -State Completed).Count
529 | }
530 | Write-Progress -Activity $Activity -Completed
531 | }
532 |
533 | #Start logging.
534 | Start-Logging -LogDirectory "C:\ScriptLogs\Disable-InactiveADAccounts" -LogName "Disable-InactiveADAccounts" -LogRetentionDays 30
535 |
536 | #Start function.
537 | . Disable-InactiveADAccounts -To @("email@domain.com","email2@domain.com") -From "noreply@domain.com" -SMTPServer "server.domain.local" -UTCSkew -5 -OutputDirectory "C:\ScriptLogs\Disable-InactiveADAccounts" -ExclusionGroup @("ServiceAccts") -ReportOnly
538 |
539 | #Stop logging.
540 | Write-Output ("Stop time: " + (Get-Date))
541 | Stop-Transcript
--------------------------------------------------------------------------------
/Disable-InactiveADComputers.ps1:
--------------------------------------------------------------------------------
1 | ####################################################
2 | #
3 | # Title: Disable-InactiveADComputers
4 | # Date Created : 2017-09-22
5 | # Last Edit: 2018-02-14
6 | # Author : Andrew Ellis
7 | # GitHub: https://github.com/AndrewEllis93/PowerShell-Scripts
8 | #
9 | # Notes:
10 | # This finds the last logon for all AD computers and disables any that have been inactive for X number of days (depending on what threshold you set).
11 | # The difference with this script is that it gets the most accurate last logon available by comparing the results from all domain controllers. By default the lastlogontimestamp is only replicated every 14 days minus a random percentage of 5. This makes it much more accurate, but also it is a very slow. It also supports an exclusion AD group that you can put things like service computers in to prevent them from being disabled. It will also email a report to the specified email addresses.
12 | # WARNING: THIS SCRIPT WILL OVERWRITE EXTENSIONATTRIBUTE3 FOR ALL COMPUTER OBJECTS, MAKE SURE YOU ARE NOT USING IT FOR ANYTHING ELSE
13 | # 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.
14 | #
15 | ####################################################
16 |
17 | #Function declarations
18 | Function Start-Logging {
19 | <#
20 | .SYNOPSIS
21 | This function starts a transcript in the specified directory and cleans up any files older than the specified number of days.
22 |
23 | .DESCRIPTION
24 | Please ensure that the log directory specified is empty, as this function will clean that folder.
25 |
26 | .EXAMPLE
27 | Start-Logging -LogDirectory "C:\ScriptLogs\LogFolder" -LogName $LogName -LogRetentionDays 30
28 |
29 | .LINK
30 | https://github.com/AndrewEllis93/PowerShell-Scripts
31 |
32 | .NOTES
33 | Author: Andrew Ellis
34 | #>
35 | Param (
36 | [Parameter(Mandatory=$true)]
37 | [String]$LogDirectory,
38 | [Parameter(Mandatory=$true)]
39 | [String]$LogName,
40 | [Parameter(Mandatory=$true)]
41 | [Int]$LogRetentionDays
42 | )
43 |
44 | #Sets screen buffer from 120 width to 500 width. This stops truncation in the log.
45 | $ErrorActionPreference = 'SilentlyContinue'
46 | $pshost = Get-Host
47 | $pswindow = $pshost.UI.RawUI
48 |
49 | $newsize = $pswindow.BufferSize
50 | $newsize.Height = 3000
51 | $newsize.Width = 500
52 | $pswindow.BufferSize = $newsize
53 |
54 | $newsize = $pswindow.WindowSize
55 | $newsize.Height = 50
56 | $newsize.Width = 500
57 | $pswindow.WindowSize = $newsize
58 | $ErrorActionPreference = 'Continue'
59 |
60 | #Remove the trailing slash if present.
61 | If ($LogDirectory -like "*\") {
62 | $LogDirectory = $LogDirectory.SubString(0,($LogDirectory.Length-1))
63 | }
64 |
65 | #Create log directory if it does not exist already
66 | If (!(Test-Path $LogDirectory)) {
67 | New-Item -ItemType Directory $LogDirectory -Force | Out-Null
68 | }
69 |
70 | $Today = Get-Date -Format M-d-y
71 | Start-Transcript -Append -Path ($LogDirectory + "\" + $LogName + "." + $Today + ".log") | Out-Null
72 |
73 | #Shows proper date in log.
74 | Write-Output ("Start time: " + (Get-Date))
75 |
76 | #Purges log files older than X days
77 | $RetentionDate = (Get-Date).AddDays(-$LogRetentionDays)
78 | Get-ChildItem -Path $LogDirectory -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $RetentionDate -and $_.Name -like "*.log"} | Remove-Item -Force
79 | }
80 | Function Disable-InactiveADComputers {
81 | <#
82 | .SYNOPSIS
83 | This script disables AD computers older than the threshold (in days) and stamps them in ExtensionAttribute3 with the disabled date. It also sends an email report.
84 |
85 | .DESCRIPTION
86 | Make sure you read through the comments (as with all of these scripts). It just finds the last logon for all AD computers and disables any that have been inactive for X number of days (depending on what threshold you set). The difference with this script is that it gets the most accurate last logon available by comparing the results from all domain controllers. By default the lastlogontimestamp is only replicated every 14 days minus a random percentage of 5. This makes it much more accurate. It also supports an exclusion AD group that you can put things like service computers in to prevent them from being disabled. It will also email a report to the specified email addresses.
87 | "-ReportOnly" will skip actually disabling the AD computers and just send an email report of inactivity instead.
88 |
89 | .EXAMPLE
90 | Disable-InactiveADComputers -To @("email@domain.com","email2@domain.com") -From "noreply@domain.com" -SMTPServer "server.domain.local" -UTCSkew -5 -OutputDirectory "C:\ScriptLogs\Disable-InactiveADComputers" -ExclusionGroup @('ServiceComputers','Auto-Disable Exclusions') -DaysThreshold 30 -ReportOnly $True
91 |
92 | .LINK
93 | https://github.com/AndrewEllis93/PowerShell-Scripts
94 |
95 | .NOTES
96 | Author: Andrew Ellis
97 | #>
98 |
99 | Param(
100 | #From address for email reports.
101 | [Parameter(Mandatory=$true)]
102 | [String]$From,
103 |
104 | #If $true, email report will be sent without disabling or stamping any AD computers.
105 | [switch]$ReportOnly = $False,
106 |
107 | #SMTP server for sending reports.
108 | [Parameter(Mandatory=$true)]
109 | [String]$SMTPServer,
110 |
111 | #Array. You can add more than one entry.
112 | [Parameter(Mandatory=$true)]
113 | [Array]$To,
114 |
115 | #Accounting for the time zone difference, since some results are given in UTC. Eastern time is UTC-5.
116 | [Parameter(Mandatory=$true)]
117 | [Int]$UTCSkew,
118 |
119 | #Threshold of days of inactivity before disabling the computer. Defaults to 30 days.
120 | [Int]$DaysThreshold = 30,
121 |
122 | #Where to export CSVs etc.
123 | [Parameter(Mandatory=$true)]
124 | [String]$OutputDirectory,
125 |
126 | #Subject for email reports.
127 | [String]$Subject = "Computer Cleanup Report",
128 |
129 | #Amount of times to try for identical DC results before giving up. 30 second retry delay after each failure.
130 | [Int]$MaxTryCount = 20,
131 |
132 | #AD group containing computers to exclude.
133 | [array]$ExclusionGroups
134 | )
135 |
136 | #Remove trailing slash if present.
137 | If ($OutputDirectory -like "*\") {
138 | $OutputDirectory = $OutputDirectory.substring(0,($OutputDirectory.Length-1))
139 | }
140 |
141 | #RE-ENABLED ACCOUNT FLAGGING
142 | #Gets all AD computers with ExtensionAttribute3 set to an inactivity or disablement date and sets it to "RE-ENABLED ON "
143 | Write-Output ""
144 | Write-Output "RE-ENABLED ACCOUNT FLAGGING:"
145 | Write-Output "Finding unflagged re-enabled computers..."
146 |
147 | $ReEnabledComputers = Get-ADComputer -Filter * -Properties DistinguishedName,ExtensionAttribute3,SamAccountName,Enabled | Where-Object {
148 | $_.Enabled -eq $True -and
149 | (
150 | $_.ExtensionAttribute3 -like "DISABLED ON*" -or
151 | $_.ExtensionAttribute3 -like "INACTIVE SINCE*"
152 | )
153 | }
154 |
155 | Write-Output (($ReEnabledComputers.SamAccountName.Count).ToString() + " computers were found.")
156 |
157 | $Date = "RE-ENABLED ON " + (Get-Date)
158 |
159 | ForEach ($ReEnabledComputer in $ReEnabledComputers){
160 | Write-Output ("Setting ExtensionAttribute3 re-enabled flag for " + $ReEnabledComputer.SamAccountName + "...")
161 | If ($ReportOnly){
162 | Set-ADComputer -Identity $ReEnabledComputer.SamAccountName -Replace @{ExtensionAttribute3=$Date} -WhatIf
163 | }
164 | Else {
165 | Set-ADComputer -Identity $ReEnabledComputer.SamAccountName -Replace @{ExtensionAttribute3=$Date}
166 | }
167 | }
168 |
169 | #OLD RE-ENABLED ACCOUNT CLEANUP
170 | #Gets all computers with ExtensionAttribute3 set to an expired re-enable date to clear it.
171 | Write-Output ""
172 | Write-Output 'CLEANUP - EXPIRED "RE-ENABLED" FLAGS:'
173 | Write-Output 'Finding computers with expired "re-enabled" flags...'
174 |
175 | $ExpiredFlagComputers = Get-ADComputer -Filter * -Properties DistinguishedName,ExtensionAttribute3,SamAccountName,Enabled | Where-Object {
176 | $_.ExtensionAttribute3 -like "RE-ENABLED*" -and
177 | [datetime]($_.ExtensionAttribute3 -replace 'RE-ENABLED ON ', '') -lt (Get-Date).AddDays(-$DaysThreshold)
178 | }
179 |
180 | Write-Output (($ExpiredFlagComputers.SamAccountName.Count).ToString() + " computers were found.")
181 |
182 | ForEach ($ExpiredFlagComputer in $ExpiredFlagComputers){
183 | Write-Output ("Clearing ExtensionAttribute3 re-enabled flag for " + $ExpiredFlagComputer.SamAccountName + "...")
184 | If ($ReportOnly){
185 | Set-ADComputer -Identity $ExpiredFlagComputer.SamAccountName -Clear ExtensionAttribute3 -WhatIf
186 | }
187 | Else {
188 | Set-ADComputer -Identity $ExpiredFlagComputer.SamAccountName -Clear ExtensionAttribute3
189 | }
190 | }
191 |
192 | #INACTIVE COMPUTER DISABLEMENT AND FLAGGING
193 | #Declare try count at 0.
194 | $TryCount= 0
195 |
196 | #Get all DCs, add array names to vars array
197 | $DCnames = (Get-ADGroupMember 'Domain Controllers').Name | Sort-Object
198 |
199 | #This just tests if we already have results for each DC, in case we are running this twice in the same session (mostly just for testing).
200 | $ExistingResults = @(0) * $DCnames.Count
201 | $TestIteration = 0
202 | $DCnames | ForEach-Object {
203 | If (Get-Variable -Name $_ -ErrorAction SilentlyContinue){
204 | $ExistingResults[$TestIteration] = $True
205 | }
206 | Else {
207 | $ExistingResults[$TestIteration] = $False
208 | }
209 | $TestIteration++
210 | }
211 |
212 | #Check that results match from each DC by comparing all results in order. Retry if there is a mismatch, up to the MaxTryCount (default 20)
213 | While (($ComparisonResults -contains $False -or !$ComparisonResults) -and $TryCount -lt $MaxTryCount){
214 | #Makes sure we don't have any left over jobs from another run
215 | Get-Job | Stop-Job
216 | Get-Job | Remove-Job
217 |
218 | If ((!$ExistingResults -or $ExistingResults -contains $False) -or ($ComparisonResults -contains $False -or !$ComparisonResults)){
219 | #Fetch AD computers from each DC, add to named array
220 | Write-Output ""
221 | Write-Output "Starting data retrieval jobs..."
222 |
223 | ForEach ($DCName in $DCnames) {
224 | Start-Job -Name $DCName -ArgumentList $DCName -ScriptBlock {
225 | param($DCName)
226 | #Get AD results
227 | Import-Module ActiveDirectory
228 | $Results = Get-ADComputer -Filter {Enabled -eq $True} -Server $DCName -Properties DistinguishedName,LastLogon,LastLogonTimestamp,whenCreated,Description,ExtensionAttribute3 -ErrorAction Stop
229 | $Results = $Results | Sort-Object -Property SamAccountName
230 | Return $Results
231 | }
232 | }
233 |
234 | #Wait for jobs to complete, show progress bar
235 | Wait-JobsWithProgress -Activity "Retrieving and sorting results from each DC. Please be patient"
236 |
237 | #Put results into named arrays for each DC
238 | ForEach ($DCName in $DCnames) {
239 | Set-Variable -Name $DCName -Value (Receive-Job -Name $DCName)
240 | }
241 | }
242 |
243 | $ComparisonResults = @()
244 |
245 | ForEach ($i in 0..(($DCnames.Count)-1)){
246 | If ($i -le (($DCnames.Count)-2)){
247 | Write-Output ("Comparing results from " + $DCnames[$i] + " and " + $DCnames[$i+1] + "...")
248 | $NotEqual = Compare-Object (Get-Variable -Name $DCnames[$i]).Value (Get-Variable -Name $DCnames[$i+1]).Value -Property SamAccountName
249 |
250 | If (!$NotEqual) {
251 | $ComparisonResults += $True
252 | }
253 | Else {
254 | $ComparisonResults += $False
255 | }
256 | }
257 | }
258 | If ($ComparisonResults -contains $False){
259 | Write-Warning "One or more DCs returned differing results. This is likely just replication delay. Retrying..."
260 | $TryCount++
261 | }
262 | }
263 | If ($TryCount -lt $MaxTryCount){
264 | Write-Output "All DC results are identical!"
265 | }
266 | Else {
267 | Throw "Try limit exceeded. Aborting."
268 | }
269 |
270 | #Removes the completes jobs.
271 | Get-Job | Remove-Job
272 |
273 | #Convert our results into hash tables because they are MUCH faster to process than PSObjects.
274 | If (!$ExistingResults -or $ExistingResults -contains $False){
275 | Write-Output ""
276 | Write-Output "Starting hash table conversions..."
277 | Write-Output ""
278 | ForEach ($DCName in $DCnames) {
279 | [array]$Data = (Get-Variable -Name $DCName).Value
280 | $Count = (Get-Variable -Name $DCName).Value.Count
281 |
282 | Start-Job -Name $DCName -ArgumentList $Data,$Count -ScriptBlock {
283 | param(
284 | [array]$Data,
285 | $Count
286 | )
287 | #Function to convert objects to hash tables
288 | #Credit: https://gist.github.com/dlwyatt/4166704557cf73bdd3ae
289 | Function ConvertTo-Hashtable{
290 | [CmdletBinding()]
291 | Param (
292 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
293 | [psobject[]] $InputObject
294 | )
295 | Process{
296 | ForEach ($object in $InputObject){
297 | $hash = @{}
298 |
299 | ForEach ($property in $object.PSObject.Properties){
300 | $hash[$property.Name] = $property.Value
301 | }
302 |
303 | $hash
304 | }
305 | }
306 | }
307 | #Declare the results array with empty hash tables to put the hash table objects into.
308 | [array]$HashResults = @(@{}) * $Count
309 |
310 | #Loop through each object, convert to hash table, add to HashResults array.
311 | $Iteration = 0
312 | $Data | ForEach-Object {
313 | $HashResults[$Iteration] = $_ | ConvertTo-Hashtable
314 | $Iteration++
315 | }
316 | Return $HashResults
317 | }
318 | }
319 | }
320 |
321 | #Wait for jobs to complete, show progress bar
322 | Wait-JobsWithProgress -Activity "Converting results to hash tables"
323 |
324 | #Get the hash table results from the jobs.
325 | If (!$ExistingResults -or $ExistingResults -contains $False){
326 | ForEach ($DCName in $DCNames){
327 | Set-Variable -Name $DCName -Value (Receive-Job -Name $DCName) -Force
328 | }
329 | }
330 |
331 | #Get current time for comparison later.
332 | $StartTime = Get-Date
333 |
334 | #Computer count so we know how many times to loop.
335 | $ComputerCount = (Get-Variable -Name $DCnames[0]).Value.Count
336 |
337 | #Create results array of the same size
338 | $FullResults = @($null) * $ComputerCount
339 |
340 | #Loop through array indexes
341 | ForEach ($i in 0..($ComputerCount -1)){
342 | $ReEnabledDate = $null
343 |
344 | #Grab computer object from each resultant array, make array of each computer object
345 | $ComputerEntries = @(@{}) * $DCnames.Count
346 | ForEach ($o in 0..($DCnames.Count -1)) {
347 | $ComputerEntries[$o] = (Get-Variable -Name $DCnames[$o]).Value[$i]
348 | }
349 |
350 | #If that computer's array contains a mismatch, bail. This should realistically never happen because we already compared the arrays.
351 | If (($ComputerEntries.SamAccountName | Select-Object -Unique).Count -gt 1){
352 | Throw "A computer mismatch at index $i has occurred. Aborting."
353 | }
354 |
355 | #Find most recent LastLogon, whenCreated, and LastLogonTimestamps, cast to datetimes.
356 | If ($ComputerEntries.LastLogon){
357 | [datetime]$LastLogon = [datetime]::FromFileTimeUtc(($ComputerEntries.LastLogon | Measure-Object -Maximum).Maximum)
358 | $LastLogon = $LastLogon.AddHours($UTCSkew)
359 | [datetime]$TrueLastLogon = $LastLogon
360 | }
361 | Else {[datetime]$LastLogon = 0; $TrueLastLogon = 0}
362 |
363 | [datetime]$whenCreated = $ComputerEntries[0].whenCreated
364 |
365 | If ($ComputerEntries.LastLogonTimestamp){
366 | [datetime]$LastLogonTimestamp = [datetime]::FromFileTimeUtc(($ComputerEntries.LastLogonTimestamp | Measure-Object -Maximum).Maximum)
367 | $LastLogonTimestamp = $LastLogonTimestamp.AddHours($UTCSkew)
368 | }
369 | Else {[datetime]$LastLogonTimestamp = 0}
370 |
371 | #If LastLogonTimestamp is newer, use that instead of LastLogon. Realistically this should never happen, but just in case.
372 | If ($LastLogonTimestamp -gt $LastLogon){
373 | $TrueLastLogon = $LastLogonTimestamp
374 | }
375 |
376 | #If there is no last logon available from any attributes, or it is older than 20 years (essentially null/zero), use the date created instead.
377 | If ($TrueLastLogon -eq 0 -or !$TrueLastLogon -or (New-TimeSpan -Start $TrueLastLogon -End $StartTime).Days -gt 7300){
378 | [datetime]$TrueLastLogon = $whenCreated
379 | }
380 |
381 | #If the account was flagged as re-enabled, take that into consideration too.
382 | If ($ComputerEntries.ExtensionAttribute3 -like "RE-ENABLED ON*"){
383 | $ReEnabledDate = [datetime](($ComputerEntries.ExtensionAttribute3 | Measure-Object -Maximum).Maximum -replace 'RE-ENABLED ON ', '')
384 | If ($ReEnabledDate -gt $TrueLastLogon){
385 | $TrueLastLogon = $ReEnabledDate
386 | }
387 | }
388 |
389 | #Calculate days of inactivity.
390 | $DaysInactive = (New-TimeSpan -Start $TrueLastLogon -End $StartTime).Days
391 |
392 | #Create object for output array
393 | $OutputObj = [PSCustomObject]@{
394 | SamAccountName=$ComputerEntries[0].SamAccountName
395 | Enabled=$ComputerEntries[0].Enabled
396 | LastLogon=$TrueLastLogon
397 | WhenCreated=$whenCreated
398 | DaysInactive=$DaysInactive
399 | Name=$ComputerEntries[0].Name
400 | DistinguishedName=$ComputerEntries[0].DistinguishedName
401 | Description=$ComputerEntries[0].Description
402 | ReEnabledDate=$ReEnabledDate
403 | }
404 |
405 | #Append object to output array and output progress to console.
406 | $FullResults[$i] = $OutputObj
407 | $PercentComplete = [math]::Round((($i/$ComputerCount) * 100),2)
408 | Write-Output ("Computer: " + $OutputObj.SamAccountName + " - Last logon: $TrueLastLogon ($DaysInactive day(s) inactivity) - $PercentComplete% complete.")
409 | }
410 |
411 | #Gets exlusions, error action is set to stop
412 | If ($ExclusionGroups){
413 | $ComputerExclusions = @()
414 | ForEach ($ExclusionGroup in $ExclusionGroups){
415 | Write-Output "Getting `"$ExclusionGroup`" members..."
416 | $ComputerExclusions += (Get-ADGroupMember -Identity $ExclusionGroup -ErrorAction Stop).SamAccountName
417 | }
418 | }
419 |
420 | #Filter
421 | Write-Output "Filtering computers..."
422 | $FilteredComputersResults = $FullResults | Where-Object {$ComputerExclusions -notcontains $_.SamAccountName}
423 | $FullResults = $FullResults | Where-Object {$_ -ne $null}
424 |
425 | #For some reason compare-object is not working properly without specifying all properties. Don't know why.
426 | $ExcludedComputersResults = Compare-Object $FilteredComputersResults $FullResults `
427 | -Property SamAccountName,enabled,lastlogon,whencreated,DaysInactive,name,distinguishedname,Description,ExtensionAttribute3 |
428 | Select-Object SamAccountName,enabled,lastlogon,whencreated,DaysInactive,name,distinguishedname,Description,ExtensionAttribute3
429 |
430 | #Add to ComputersDisabled array for CSV report. Also disable and stamp computers if ReportOnly is set to false (default).
431 | $InactiveComputersDisabled = @()
432 | If (!$ReportOnly){
433 | $FilteredComputersResults | ForEach-Object {
434 | If ($_.DaysInactive -ge $DaysThreshold){
435 | Write-Output ("Disabling " + $_.SamAccountName + "...")
436 | Disable-ADAccount -Identity $_.SamAccountName
437 | $Date = "INACTIVE SINCE " + $_.LastLogon
438 | Set-ADComputer -Identity $_.SamAccountName -Replace @{ExtensionAttribute3=$Date}
439 | $InactiveComputersDisabled += $_
440 | }
441 | }
442 | }
443 | Else {
444 | $FilteredComputersResults | ForEach-Object {
445 | If ($_.DaysInactive -ge $DaysThreshold){
446 | Write-Output ("Disabling " + $_.SamAccountName + "...")
447 | Disable-ADAccount -Identity $_.SamAccountName -WhatIf
448 | $Date = "INACTIVE SINCE " + $_.LastLogon
449 | Set-ADComputer -Identity $_.SamAccountName -Replace @{ExtensionAttribute3=$Date} -WhatIf
450 | $InactiveComputersDisabled += $_
451 | }
452 | }
453 | }
454 |
455 | #Filtered computers - add to ComputersNotDisabled array for CSV report
456 | $ExcludedInactiveComputers = @()
457 | $ExcludedComputersResults | ForEach-Object {
458 | If ($_.DaysInactive -ge $DaysThreshold){
459 | $ExcludedInactiveComputers += $_
460 | }
461 | }
462 |
463 | #Create output directory if it does not exist
464 | If (!(Test-Path $OutputDirectory)){
465 | New-Item -ItemType Directory $OutputDirectory
466 | }
467 |
468 | #Form the paths for the output files
469 | $InactiveComputersDisabledCSV = $OutputDirectory + "\InactiveComputers-Disabled.csv"
470 | $InactiveComputersExcludedCSV = $OutputDirectory + "\InactiveComputers-Excluded.csv"
471 |
472 | #Export the CSVs
473 | $InactiveComputersDisabled | Export-CSV $InactiveComputersDisabledCSV -NoTypeInformation -Force
474 | If ($ExclusionGroups){
475 | $ExcludedInactiveComputers | Export-CSV $InactiveComputersExcludedCSV -NoTypeInformation -Force
476 | }
477 |
478 | #Send email with CSVs as attachments
479 | Write-Output "Sending email..."
480 |
481 | If ($ExclusionGroups){
482 | $ExclusionGroupsList = $ExclusionGroups -join ", "
483 | $Body = @"
484 | Attached are two reports:
485 | - InactiveComputers-Disabled.csv: AD computers that were disabled for inactivity ($DaysThreshold days).
486 | - InactiveComputers-Excluded.csv: Inactive AD computers that were excluded from disablement.
487 |
488 | Excluded AD group(s): $ExclusionGroupsList
489 | "@
490 | }
491 | Else {
492 | $Body = "Attached is a list of AD computers that were disabled for inactivity ($DaysThreshold days)."
493 |
494 | }
495 |
496 | If ($ExclusionGroups) {
497 | Send-MailMessage -Attachments @($InactiveComputersDisabledCSV,$InactiveComputersExcludedCSV) -From $From -SmtpServer $SMTPServer -To $To -Subject $Subject -Body $Body
498 | }
499 | Else {
500 | Send-MailMessage -Attachments @($InactiveComputersDisabledCSV) -From $From -SmtpServer $SMTPServer -To $To -Subject $Subject -Body $Body
501 | }
502 |
503 | <#
504 | # This is here if you want to use it in conjunction with my Move-Disabled script. Just uncomment and replace with your scheduled task path.
505 | Write-Output "Starting Move-Disabled task..."
506 | Start-ScheduledTask -TaskName "\Move-Disabled"
507 | #>
508 | }
509 |
510 | Function Wait-JobsWithProgress {
511 | param(
512 | [Parameter(Mandatory=$true)]
513 | [string]$Activity
514 | )
515 | # SHOW JOB PROGRESS
516 | $Total = (Get-Job).Count
517 | $CompletedJobs = (Get-Job -State Completed).Count
518 |
519 | # Loop while there are running jobs
520 | While ($CompletedJobs -ne $Total) {
521 | # Update progress based on how many jobs are done yet.
522 | # Write-Output "Waiting for background jobs: $CompletedJobs/$Total"
523 | Write-Progress -Activity $Activity -PercentComplete (($CompletedJobs/$Total)*100) -Status "$CompletedJobs/$Total jobs completed"
524 |
525 | # After updating the progress bar, get current job count
526 | $CompletedJobs = (Get-Job -State Completed).Count
527 | }
528 | Write-Progress -Activity $Activity -Completed
529 | }
530 |
531 | #Start logging.
532 | Start-Logging -LogDirectory "C:\ScriptLogs\Disable-InactiveADComputers" -LogName "Disable-InactiveADComputers" -LogRetentionDays 30
533 |
534 | #Start function.
535 | . Disable-InactiveADComputers -To @("email@domain.com","email2@domain.com") -From "noreply@domain.com" -SMTPServer "server.domain.local" -UTCSkew -5 -DaysThreshold 60 -OutputDirectory "C:\ScriptLogs\Disable-InactiveADComputers" -ExclusionGroup @("ServiceAccts") -ReportOnly
536 |
537 | #Stop logging.
538 | Write-Output ("Stop time: " + (Get-Date))
539 | Stop-Transcript
--------------------------------------------------------------------------------
/Discover-DriveSpace.ps1:
--------------------------------------------------------------------------------
1 | ####################################################
2 | #
3 | # Title: Discover-DriveSpace
4 | # Date Created : 2017-12-28
5 | # Last Edit: 2017-12-29
6 | # Author : Andrew Ellis
7 | # GitHub: https://github.com/AndrewEllis93/PowerShell-Scripts
8 | #
9 | # This function gets all your servers in AD and dumps the drives with sizes and remaining free space to a CSV (DriveSpace.csv).
10 | # It also export some other files - Pingable.txt, PingFail.txt, and Servers.csv. Those should be self-explanatory.
11 | #
12 | ####################################################
13 |
14 | #Function declarations
15 | Function Discover-DriveSpace {
16 | <#
17 | .SYNOPSIS
18 | This function gets all your servers in AD and dumps the drives with sizes and remaining free space to a CSV (DriveSpace.csv).
19 | It also export some other files - Pingable.txt, PingFail.txt, and Servers.csv. Those should be self-explanatory.
20 |
21 | .DESCRIPTION
22 |
23 | .EXAMPLE
24 | Discover-DriveSpace -OutputPath "C:\temp" -DomainController "DC1.domain.local"
25 |
26 | .LINK
27 | https://github.com/AndrewEllis93/PowerShell-Scripts
28 |
29 | .NOTES
30 | Author: Andrew Ellis
31 | #>
32 |
33 | Param (
34 | [Parameter(Mandatory=$true)][string]$OutputPath,
35 | [string]$DomainController
36 | )
37 |
38 | $Output = @()
39 | $Pingable = @()
40 | $PingFail = @()
41 | $Iteration = 0
42 |
43 | #Remove trailing slash if present.
44 | If ($OutputPath -like "*\"){$OutputPath = $OutputPath.substring(0,($OutputPath.Length -1))}
45 |
46 | #Create the directory if it does not exist.
47 | If (!(Test-Path $OutputPath)){
48 | Write-Output ("Output directory not found. Creating folder at $OutputPath...")
49 | mkdir $OutputPath -Force | Out-Null
50 | }
51 |
52 | #Get servers from AD, using domain controller specified if specified.
53 | Import-Module ActiveDirectory
54 | Write-Output "Getting servers from AD..."
55 | If (!$DomainController){$ADServers = Get-ADComputer -Filter {OperatingSystem -Like "Windows Server*"} | Where-Object {$_.Enabled -eq "True"} | Select-Object Name | Sort-Object Name}
56 | Else {$ADServers = Get-ADComputer -Server $DomainController -Filter {OperatingSystem -Like "Windows Server*"} | Where-Object {$_.Enabled -eq "True"} | Select-Object Name | Sort-Object Name}
57 |
58 | $Count = $ADServers.Count
59 |
60 | Clear-Host
61 | "`n`n`n`n`n`n`n`n"
62 |
63 | ForEach ($Server in $ADServers){
64 | #Show progress
65 | $PercentComplete = [math]::Round((($Iteration / $Count) * 100),0)
66 | If ($PercentComplete -lt 100){Write-Progress -Activity ("Getting disk info from " + $Server.Name + "..") -Status "$PercentComplete% Complete ($Iteration/$Count)" -PercentComplete $PercentComplete}
67 | Else {Write-Progress -Activity ("Getting disk info from" + $Server.Name + "..") -Status "$PercentComplete% Complete ($Iteration/$Count)" -PercentComplete $PercentComplete -Completed}
68 |
69 | #Tests ping. Only tries a second time if first ping fails.
70 | If (Test-Connection -ComputerName $Server.Name -Count 1 -ErrorAction SilentlyContinue){$Ping = $True}
71 | ElseIf (Test-Connection -ComputerName $Server.Name -Count 1 -ErrorAction SilentlyContinue){$Ping = $True}
72 | Else {$Ping = $False}
73 |
74 | #If pingable...
75 | If ($Ping){
76 |
77 | #Add to pingable servers array
78 | $Pingable += $Server.Name
79 |
80 | #Get disk info from WMI
81 | $DiskInfo = Get-WMiObject -ComputerName $Server.Name win32_logicaldisk -Filter "drivetype=3 AND NOT Volumename LIKE '%page%'" -ErrorAction SilentlyContinue
82 |
83 | #Create an object for each disk returned from WMI
84 | ForEach ($Disk in $DiskInfo){
85 | $Size = [math]::Round(($Disk.Size/1gb),2)
86 | $FreeSpace = [math]::Round(($Disk.FreeSpace/1gb),2)
87 | $PercentFree = [math]::Round((($Disk.FreeSpace * 100.0)/$Disk.Size),2)
88 |
89 | $Obj = New-Object -TypeName PSObject
90 | $Obj | Add-Member -MemberType NoteProperty -Name "SystemName" -Value $Disk.SystemName
91 | $Obj | Add-Member -MemberType NoteProperty -Name "DeviceID" -Value $Disk.DeviceID
92 | $Obj | Add-Member -MemberType NoteProperty -Name "SizeGB" -Value $Size
93 | $Obj | Add-Member -MemberType NoteProperty -Name "FreeSpaceGB" -Value $FreeSpace
94 | $Obj | Add-Member -MemberType NoteProperty -Name "PercentFree" -Value $PercentFree
95 | $Obj | Add-Member -MemberType NoteProperty -Name "Label" -Value $Disk.Volumename
96 |
97 | #Add to output array.
98 | $Output += $Obj
99 | }
100 | }
101 | Else {
102 | Write-Warning ("Ping failed for " + $Server.Name + ".")
103 | $PingFail += $Server.Name
104 | }
105 | $Iteration++
106 | }
107 |
108 | Write-Output ("Exporting files to $OutputPath...")
109 | $ADServers | ConvertTo-CSV -NoTypeInformation | Select-Object -Skip 1 | Out-File "$OutputPath\Servers.csv"
110 | $Pingable | Out-File "$OutputPath\Pingable.txt"
111 | $PingFail | Out-File "$OutputPath\PingFail.txt"
112 | $Output | Sort-Object SystemName,Drive | Export-CSV "$OutputPath\DriveSpace.csv" -NoTypeInformation
113 |
114 | Write-Output "Done."
115 | }
116 |
117 | #Call the function.
118 | Discover-DriveSpace -OutputPath "C:\Temp"
--------------------------------------------------------------------------------
/Discover-Shares.ps1:
--------------------------------------------------------------------------------
1 | ####################################################
2 | #
3 | # Title: Discover-Shares
4 | # Date Created : 2017-10-31
5 | # Last Edit: 2017-12-29
6 | # Author : Andrew Ellis
7 | # GitHub: https://github.com/AndrewEllis93/PowerShell-Scripts
8 | #
9 | # This function discovers all Windows Servers from Active Directory and discovers their file shares using WMI.
10 | #
11 | ####################################################
12 |
13 | Function Discover-Shares {
14 | <#
15 | .SYNOPSIS
16 | This function discovers all Windows Servers from Active Directory and discovers their file shares using WMI.
17 |
18 | .DESCRIPTION
19 | The following are always excluded:
20 | - Admin shares
21 | - NETLOGON
22 | - SYSVOL
23 | - print$
24 | - prnproc$
25 | - ADMIN$
26 |
27 | FilterShares is enabled by default.
28 | Filter removes:
29 | - *Sophos*
30 | - *SMS*
31 | - Wsus*
32 | - SHARES
33 | - REMINST
34 | - *ClusterStorage$
35 | - *SCCM*
36 |
37 | .EXAMPLE
38 | Find-Shares -DomainController DC1 -FilterShares $True
39 |
40 | .LINK
41 | https://github.com/AndrewEllis93/PowerShell-Scripts
42 |
43 | .NOTES
44 | Author: Andrew Ellis
45 | #>
46 |
47 | param([boolean]$FilterShares = $True,
48 | [string]$DomainContoller)
49 |
50 | If (!$DomainController){$Servers = Get-ADComputer -Filter {OperatingSystem -Like "Windows Server*"} | Where-Object {$_.Name -notlike "ENTDP*"} | Sort-Object Name}
51 | Else {$Servers = Get-ADComputer -Server $DomainContoller -Filter {OperatingSystem -Like "Windows Server*"} | Where-Object {$_.Name -notlike "ENTDP*"} | Sort-Object Name}
52 | $ServerCount = $Servers.Count
53 | $Iteration = 1
54 |
55 | $Output = @()
56 | $FailServers = @()
57 |
58 | $Servers | ForEach-Object {
59 | $Server = $_.Name
60 |
61 | $Fail = $False
62 | $WMI = $null
63 |
64 | Try {
65 | if ($FilterShares){
66 | $WMI = get-WmiObject -class Win32_Share -computer $_.Name -ErrorAction Stop | Where-Object {`
67 | $_.Name -notlike "?$" -and `
68 | $_.Name -notlike "*Sophos*" -and `
69 | $_.Name -notlike "*SCCM*" -and `
70 | $_.Name -notlike "*ClusterStorage$" -and `
71 | $_.Name -notlike "SMS*" -and `
72 | $_.Name -notlike "Wsus*" -and `
73 | $_.Name -ne "ADMIN$" -and `
74 | $_.Name -ne "print$" -and `
75 | $_.Name -ne "prnproc$" -and `
76 | $_.Name -ne "NETLOGON" -and `
77 | $_.Name -ne "SYSVOL" -and `
78 | $_.Name -ne "SHARES" -and `
79 | $_.Name -ne "REMINST" -and `
80 | $_.Path -like "?:\*"}
81 | }
82 | Else {
83 | $WMI = get-WmiObject -class Win32_Share -computer $_.Name -ErrorAction Stop | Where-Object {`
84 | $_.Name -notlike "?$" -and `
85 | $_.Path -like "?:\*"}
86 | }
87 | }
88 | Catch {
89 | $Fail = $True
90 | Write-Warning ($Server + " discovery failed.")
91 | Write-Error $Error[0]
92 | }
93 |
94 | If ($WMI){
95 | $WMI | ForEach-Object {
96 | $OutputObj = New-Object -TypeName PSObject
97 | $OutputObj | Add-Member -MemberType NoteProperty -Name 'Server' -Value $Server
98 | $OutputObj | Add-Member -MemberType NoteProperty -Name 'Share' -Value $_.Name
99 | $OutputObj | Add-Member -MemberType NoteProperty -Name 'Path' -Value $_.Path
100 | $OutputObj | Add-Member -MemberType NoteProperty -Name 'Description' -Value $_.Description
101 |
102 | $Output += $OutputObj
103 | }
104 | }
105 |
106 | If ($Fail){
107 |
108 | $FailServers += $_.Name
109 |
110 | $OutputObj = New-Object -TypeName PSObject
111 | $OutputObj | Add-Member -MemberType NoteProperty -Name 'Server' -Value $Server
112 | $OutputObj | Add-Member -MemberType NoteProperty -Name 'Share' -Value "FAIL"
113 | $OutputObj | Add-Member -MemberType NoteProperty -Name 'Path' -Value "FAIL"
114 | $OutputObj | Add-Member -MemberType NoteProperty -Name 'Description' -Value "FAIL"
115 |
116 | $Output += $OutputObj
117 | }
118 |
119 | $PercentComplete = [math]::Round((($Iteration / $ServerCount) * 100),0)
120 | If ($PercentComplete -lt 100){Write-Progress -Activity "Scanning AD servers for shares" -Status "$PercentComplete% Complete ($Iteration/$ServerCount)" -PercentComplete $PercentComplete}
121 | Else {Write-Progress -Activity "Scanning AD servers for shares" -Status "$PercentComplete% Complete ($Iteration/$ServerCount)" -PercentComplete $PercentComplete -Completed}
122 | $Iteration++
123 | }
124 |
125 | Return $Output
126 | }
127 |
128 | $Results = Discover-Shares -FilterShares $False
129 | $Results | Export-CSV C:\Temp\Shares.csv -NoTypeInformation
--------------------------------------------------------------------------------
/Dump-GPOs.ps1:
--------------------------------------------------------------------------------
1 | ####################################################
2 | #
3 | # Title: Dump-GPOs
4 | # Date Created : 2017-12-28
5 | # Last Edit: 2017-12-29
6 | # Author : Andrew Ellis
7 | # GitHub: https://github.com/AndrewEllis93/PowerShell-Scripts
8 | #
9 | # This exports all of your GPOs' HTML reports, a CSV detailing all the GPO links, and a txt list of all the GPOs.
10 | #
11 | ####################################################
12 |
13 | Function Dump-GPOs {
14 | <#
15 | .SYNOPSIS
16 | This exports all of your GPOs' HTML reports, a CSV detailing all the GPO links, and a txt list of all the GPOs to the specified output directory.
17 |
18 | .DESCRIPTION
19 |
20 | .EXAMPLE
21 | Dump-GPOs -OutputDirectory "C:\temp"
22 |
23 | .LINK
24 | https://github.com/AndrewEllis93/PowerShell-Scripts
25 |
26 | .NOTES
27 | Author: Andrew Ellis
28 | #>
29 |
30 | Param (
31 | [Parameter(Mandatory=$true)][String]$OutputDirectory
32 | )
33 |
34 | #Remove trailing slash if present.
35 | If ($OutputDirectory -like "*\"){$OutputDirectory = $OutputDirectory.substring(0,($OutputDirectory.Length-1))}
36 |
37 | $GPOs = get-gpo -All
38 | $AllGPOs = @()
39 |
40 | If (!(Test-Path $OutputDirectory)){mkdir $OutputDirectory -Force | Out-Null}
41 |
42 | ForEach ($GPO in $GPOs) {
43 | $GPO.DisplayName = $GPO.DisplayName.Replace('/','')
44 | $AllGPOs += $GPO.DisplayName
45 | Write-Output ("Exporting " + $OutputDirectory + "\" + $GPO.DisplayName + ".HTML...")
46 | $Path = $OutputDirectory + "\" + $GPO.DisplayName + ".HTML"
47 | Get-GPOReport -Name $GPO.DisplayName -ReportType HTML -Path $Path
48 | }
49 | $AllGPOs = $AllGPOs | Sort-Object
50 | Write-Output ("Exporting " + $OutputDirectory + "\AllGPOs.txt...")
51 | $AllGPOs | Out-File ($OutputDirectory + "\AllGPOs.txt")
52 |
53 | $OUs = Get-ADOrganizationalUnit -Filter * | Sort-Object {-join ($_.distinguishedname[($_.distinguishedname.length-1)..0])}
54 | $OutputArray = @()
55 | $OUs | ForEach-Object {
56 | $Inheritance = Get-GPInheritance -Target $_.DistinguishedName
57 |
58 | $GpoLinks = @()
59 | If ($Inheritance.GpoLinks.DisplayName){
60 | ForEach ($i in 0..($Inheritance.GpoLinks.DisplayName.Count -1)){
61 | $GpoLinks += $Inheritance.GpoLinks[$i].Order.toString() + ": " + $Inheritance.GpoLinks[$i].DisplayName
62 | }
63 | }
64 |
65 | $Obj = New-Object -TypeName PSObject
66 | $Obj | Add-Member -MemberType NoteProperty -Name "Path" -Value $Inheritance.Path
67 | $Obj | Add-Member -MemberType NoteProperty -Name "GpoLinks" -Value ($GpoLinks -join ", ")
68 | $OutputArray += $Obj
69 | }
70 |
71 | Write-Output ("Exporting " + $OutputDirectory + "\GPOLinks.csv...")
72 | $OutputArray | Export-CSV ($OutputDirectory+"\GPOLinks.csv") -NoTypeInformation
73 | }
74 |
75 | Dump-GPOs -OutputDirectory "C:\temp"
--------------------------------------------------------------------------------
/Enumerate-Access.ps1:
--------------------------------------------------------------------------------
1 | ####################################################
2 | #
3 | # Title: Enumerate-Access
4 | # Date Created : 2017-12-28
5 | # Last Edit: 2017-12-29
6 | # Author : Andrew Ellis
7 | # GitHub: https://github.com/AndrewEllis93/PowerShell-Scripts
8 | #
9 | # This function will spit back all of the permissions of a specified folder, recursively. You can choose to return inherited permissions or not. I wrote this specifically to show each and every ACL entry on a separate line. # # It's really useful for finding where a group or user is being used in NTFS ACLs. This helped us get rid of mail-enabled security groups by discovering each place that they were being used in NTFS ACLs so we could replace them.
10 | # In most cases you won't want it to return inherited permissions (it doesn't by default) so you don't get a TON of redundant output, just the explicit ACL entries.
11 | # It will generate a lot of disk activity on the target server because it scans the entire file system of the folder specified.
12 | #
13 | ####################################################
14 |
15 | Function Enumerate-Access {
16 | <#
17 | .SYNOPSIS
18 | This is a simple Powershell function to retreive all NTFS permissions recursively from a file path.
19 |
20 | .DESCRIPTION
21 | IncludeInherited defaults to False. This will only show excplicit ACL entries, excluding the top-level path which will always show all permissions.
22 | Depth is unlimited unless specified.
23 |
24 | .EXAMPLE
25 | Enumerate-Access -Path '\\SERVER\Share' -Depth 10 -IncludeInherited
26 |
27 | .LINK
28 | https://github.com/AndrewEllis93/PowerShell-Scripts
29 |
30 | .NOTES
31 | Author: Andrew Ellis
32 | #>
33 |
34 | [cmdletbinding()]
35 | Param(
36 | [Parameter(Mandatory=$true)][string]$Path,
37 | [int]$Depth,
38 | [switch]$IncludeInherited=$False
39 | )
40 |
41 | #Remove the trailing slash if present.
42 | If ($Path -like "*\"){
43 | $Path = $Path.substring(0,($Path.Length-1))
44 | }
45 | If (!(Test-Path $Path)){
46 | Throw "Path was not reachable."
47 | }
48 |
49 | #This part now has long (>260 character) path support, thanks to /u/vBurak
50 | #https://www.reddit.com/r/sysadmin/comments/7moj1w/there_was_some_interest_in_my_scripts_so_i/du18hf0/
51 | Write-Verbose "Getting file tree..."
52 | $LiteralPath = "\\?\" + $Path
53 | If ($Depth){
54 | $Tree = Get-Childitem -LiteralPath $LiteralPath -Recurse -Depth $Depth -Directory -ErrorAction
55 | }
56 | Else {
57 | $Tree = Get-Childitem -LiteralPath $LiteralPath -Recurse -Directory
58 | }
59 |
60 | $Output = [System.Collections.ArrayList]@()
61 | $Iteration = 1
62 | $Total = $Tree.Count
63 |
64 | #Top-level ACL (always shows inherited permissions)
65 | $TopLevelACL = $Path | Get-Acl
66 | $FullName = (Get-Item $Path).FullName
67 | $Index = 0
68 | $TopLevelACL.Access.IdentityReference.Value | ForEach-Object {
69 | $OutputObj = [PSCustomObject]@{}
70 | $OutputObj | Add-Member -Name FullName -MemberType NoteProperty -Value $FullName.Replace('\\?\','')
71 | $OutputObj | Add-Member -Name Owner -MemberType NoteProperty -Value $TopLevelACL.Owner
72 | $OutputObj | Add-Member -Name IdentityReference -MemberType NoteProperty -Value ($TopLevelACL.Access.IdentityReference.Value[$Index])
73 | $OutputObj | Add-Member -Name FileSystemRights -MemberType NoteProperty -Value ($TopLevelACL.Access.FileSystemRights[$Index])
74 | $OutputObj | Add-Member -Name AccessControlType -MemberType NoteProperty -Value ($TopLevelACL.Access.AccessControlType[$Index])
75 | $OutputObj | Add-Member -Name IsInherited -MemberType NoteProperty -Value ($TopLevelACL.Access.IsInherited[$Index])
76 | $OutputObj | Add-Member -Name InheritanceFlags -MemberType NoteProperty -Value ($TopLevelACL.Access.InheritanceFlags[$Index])
77 |
78 | $Output.Add($OutputObj) > $null
79 | $Index++
80 | }
81 |
82 | #Recursive ACL
83 | $Tree | ForEach-Object {
84 |
85 | $FullName = $_.FullName
86 | $ACL = $_ | Get-ACL
87 |
88 | $Index = 0
89 |
90 | If ($IncludeInherited -eq $False) {
91 | $ACL.Access.IdentityReference.Value | ForEach-Object {
92 | If ($ACL.Access.IsInherited[$Index] -eq $False){
93 | $OutputObj = [PSCustomObject]@{}
94 | $OutputObj | Add-Member -Name FullName -MemberType NoteProperty -Value $FullName.Replace('\\?\','')
95 | $OutputObj | Add-Member -Name Owner -MemberType NoteProperty -Value $ACL.Owner
96 | $OutputObj | Add-Member -Name IdentityReference -MemberType NoteProperty -Value ($ACL.Access.IdentityReference.Value[$Index])
97 | $OutputObj | Add-Member -Name FileSystemRights -MemberType NoteProperty -Value ($ACL.Access.FileSystemRights[$Index])
98 | $OutputObj | Add-Member -Name AccessControlType -MemberType NoteProperty -Value ($ACL.Access.AccessControlType[$Index])
99 | $OutputObj | Add-Member -Name IsInherited -MemberType NoteProperty -Value ($ACL.Access.IsInherited[$Index])
100 | $OutputObj | Add-Member -Name InheritanceFlags -MemberType NoteProperty -Value ($ACL.Access.InheritanceFlags[$Index])
101 |
102 | $Output.Add($OutputObj) > $null
103 | $Index++
104 | }
105 | }
106 | }
107 | Else {
108 | $ACL.Access.IdentityReference.Value | ForEach-Object {
109 | $OutputObj = [PSCustomObject]@{}
110 | $OutputObj | Add-Member -Name FullName -MemberType NoteProperty -Value $FullName.Replace('\\?\','')
111 | $OutputObj | Add-Member -Name Owner -MemberType NoteProperty -Value $ACL.Owner
112 | $OutputObj | Add-Member -Name IdentityReference -MemberType NoteProperty -Value ($ACL.Access.IdentityReference.Value[$Index])
113 | $OutputObj | Add-Member -Name FileSystemRights -MemberType NoteProperty -Value ($ACL.Access.FileSystemRights[$Index])
114 | $OutputObj | Add-Member -Name AccessControlType -MemberType NoteProperty -Value ($ACL.Access.AccessControlType[$Index])
115 | $OutputObj | Add-Member -Name IsInherited -MemberType NoteProperty -Value ($ACL.Access.IsInherited[$Index])
116 | $OutputObj | Add-Member -Name InheritanceFlags -MemberType NoteProperty -Value ($ACL.Access.InheritanceFlags[$Index])
117 |
118 | $Output.Add($OutputObj) > $null
119 | $Index++
120 | }
121 | }
122 | $PercentComplete = [math]::Round((($Iteration / $Total) * 100),1)
123 | If ($PercentComplete -lt 100){
124 | Write-Progress -Activity "Scanning permissions" -Status "$PercentComplete% Complete ($Iteration/$Total)" -PercentComplete $PercentComplete
125 | }
126 | Else {
127 | Write-Progress -Activity "Scanning permissions" -Status "$PercentComplete% Complete ($Iteration/$Total)" -PercentComplete $PercentComplete -Completed
128 | }
129 | $Iteration++
130 | }
131 | Return $Output
132 | }
133 |
134 | $ACL = Enumerate-Access -Path "C:\Test"
135 | $ACL | Export-CSV C:\TestACL.csv -NoTypeInformation -Encoding UTF8
--------------------------------------------------------------------------------
/Move-Disabled.ps1:
--------------------------------------------------------------------------------
1 | ####################################################
2 | #
3 | # Title: Move-Disabled
4 | # Date Created : 2017-12-28
5 | # Last Edit: 2018-02-22
6 | # Author : Andrew Ellis
7 | # GitHub: https://github.com/AndrewEllis93/PowerShell-Scripts
8 | #
9 | # This moves disabled computers too. It rounds up disabled accounts and ages them through different OUs (0-30 days, 30-180 days, over 180 days).
10 | #
11 | # WARNING: THIS SCRIPT WILL OVERWRITE EXTENSIONATTRIBUTE3, MAKE SURE YOU ARE NOT USING IT FOR ANYTHING ELSE
12 | #
13 | # This script will not create the OUs for you.
14 | # CREATE AN OU STRUCTURE UNDER YOUR PARENT OU AS FOLLOWS:
15 | #
16 | # Parent OU (specified by user)
17 | # -> Users
18 | # --> 0-30 Days
19 | # --> 30-180 Days
20 | # --> Over 180 Days
21 | #
22 | # -> Computers
23 | # --> 0-30 Days
24 | # --> 30-180 Days
25 | # --> Over 180 Days
26 | #
27 | ####################################################
28 |
29 | Function Move-Disabled {
30 | <#
31 | .SYNOPSIS
32 | This moves disabled users and computers. It rounds up disabled accounts and ages them through different OUs (0-30 days, 30-180 days, over 180 days).
33 |
34 | .DESCRIPTION
35 | WARNING: THIS SCRIPT WILL OVERWRITE EXTENSIONATTRIBUTE3, MAKE SURE YOU ARE NOT USING IT FOR ANYTHING ELSE
36 | ExtensionAttribute3 is used to stamp the disablement date. This function will also clear ExtensionAttribute3 for any objects that are not disabled / not in the specified OU.
37 | "ReportOnly" will not take any actions, only output the WhatIfs to the console/log. RUN THIS FIRST to get an idea of what it will do.
38 | The InactivityDays argument is optional, but is there for if you are using my Disable-InactiveADAccounts script. This is so it can account for inactivity in its calculations. Please make sure you use the same on both.
39 |
40 | .EXAMPLE
41 | Move-Disabled -ParentOU "OU=Disabled Objects,DC=domain,DC=local" -InactivityDays 30 -ComputerInactivityDays 60 -ExclusionUserGroups @('ServiceAccts') -ExclusionOUs @('OU=Test,DC=domain,DC=local','OU=Test2,DC=domain,DC=local') -DeleteComputersAt180 -DeleteUsersAt180 -ReportOnly
42 |
43 | .LINK
44 | https://github.com/AndrewEllis93/PowerShell-Scripts
45 |
46 | .NOTES
47 | Author: Andrew Ellis
48 | #>
49 |
50 | Param (
51 | [Parameter(Mandatory=$true)][string]$ParentOU,
52 | [array]$ExclusionUserGroups,
53 | [switch]$ReportOnly = $False,
54 | [int]$UserInactivityDays = 30,
55 | [int]$ComputerInactivityDays = 30,
56 | [array]$ExclusionOUs,
57 | [switch]$DeleteComputersAt180= $False,
58 | [switch]$DeleteUsersAt180= $False
59 | )
60 |
61 | #DECLARATIONS
62 | #Declares misc variables.
63 | $MovedUsers = 0 #Leave at 0.
64 | $MovedComputers = 0 #Leave at 0.
65 |
66 | #MOVE NEWLY DISABLED USERS
67 | Write-Host ""
68 | Write-Output "NEWLY DISABLED MOVE:"
69 | #Gets all newly users. msExchRecipientTypeDetails makes sure we are excluding things like shared mailboxes.
70 | Write-Output "Getting newly disabled users..."
71 | $DisabledUsers =
72 | Search-ADAccount -AccountDisabled -UsersOnly |
73 | Get-ADUser -Properties msExchRecipientTypeDetails,info,Enabled,distinguishedName,ExtensionAttribute3 |
74 | Where-Object {
75 | @(1,128,65536,2097152,2147483648,$null) -contains $_.msExchRecipientTypeDetails -and
76 | $_.DistinguishedName -notlike "*Builtin*" -and
77 | $_.DistinguishedName -notlike "*$ParentOU" -and
78 | $Exclusions.SamAccountName -notcontains $_.SamAccountName
79 | }
80 |
81 | #AD group filters if specified.
82 | If ($ExclusionUserGroups){
83 | [array]$FilterUserSAMs = @()
84 | ForEach ($ExclusionUserGroup in $ExclusionUserGroups){
85 | Write-Output "Getting $ExclusionUserGroup members..."
86 | $FilterUserSAMs += (Get-ADGroupMember $ExclusionUserGroup -ErrorAction Stop).SamAccountName
87 | }
88 | $DisabledUsers = $DisabledUsers | Where-Object {$FilterUserSAMs -notcontains $_.SamAccountName}
89 | }
90 |
91 | #OU filters if specified.
92 | If ($ExclusionOUs){
93 | [array]$FilterArray = @()
94 | ForEach ($ExclusionOU in $ExclusionOUs){
95 | $FilterArray += "`$_.DistinguishedName -NotLike `"*$ExclusionOU`""
96 | }
97 | $Filter = [scriptblock]::Create($FilterArray -join " -and ")
98 | $DisabledUsers = $DisabledUsers | Where-Object -FilterScript $Filter
99 | }
100 |
101 | Write-Output ($DisabledUsers.Count.toString() + " newly disabled user objects were found.")
102 |
103 | #Loops through the newly disabled users found.
104 | ForEach ($DisabledUser in $DisabledUsers){
105 | #Moves the user.
106 | Write-Output ("Moving user: " + $DisabledUser.SamAccountName + "...")
107 | If ($ReportOnly) {
108 | Move-ADObject -Identity $DisabledUser.DistinguishedName -TargetPath "OU=0-30 Days,OU=Users,$ParentOU" -WhatIf
109 | }
110 | Else {
111 | Move-ADObject -Identity $DisabledUser.DistinguishedName -TargetPath "OU=0-30 Days,OU=Users,$ParentOU"
112 | }
113 | $MovedUsers++
114 |
115 | #Sets the info (notes) field with the old OU.
116 | #Formats the new info field (notes). Retains old info if any exists.
117 | Write-Output ("Setting info field for user: " + $DisabledUser.SamAccountName + "...")
118 | #Gets the parent OU.
119 | $OU = $DisabledUser.DistinguishedName.Split(',',2)[1]
120 | If ([string]::IsNullOrWhiteSpace($DisabledUser.Info)){
121 | $NewInfo = "OLD OU: " + $OU
122 | }
123 | Else {
124 | $NewInfo = "OLD OU: " + $OU + "`n" + $DisabledUser.Info
125 | }
126 | If ($ReportOnly){
127 | Set-ADUser -Identity $DisabledUser.SamAccountName -Replace @{info=$NewInfo} -WhatIf
128 | }
129 | Else {
130 | Set-ADUser -Identity $DisabledUser.SamAccountName -Replace @{info=$NewInfo}
131 | }
132 |
133 | #Sets ExtensionAttribute3 for the date of disablement.
134 | If ($DisabledUser.ExtensionAttribute3 -notlike "INACTIVE*"){
135 | $Date = "DISABLED ON " + (Get-Date)
136 | Write-Output ("Setting ExtensionAttribute3 for user: " + $DisabledUser.SamAccountName + "...")
137 | If ($ReportOnly) {
138 | Set-ADUser -Identity $DisabledUser.SamAccountName -Replace @{ExtensionAttribute3=$Date} -WhatIf
139 | }
140 | Else {
141 | Set-ADUser -Identity $DisabledUser.SamAccountName -Replace @{ExtensionAttribute3=$Date}
142 | }
143 | }
144 | }
145 |
146 | #MOVE NEWLY DISABLED COMPUTERS
147 | #Get disabled computer accounts.
148 | Write-Output "Getting disabled computers..."
149 | $DisabledComputers = Get-ADComputer -Filter * -Properties Enabled,Description,distinguishedName,ExtensionAttribute3 | Where-Object {
150 | $_.Enabled -eq $False -and
151 | $_.DistinguishedName -notlike "*$ParentOU"
152 | }
153 |
154 | Write-Output ($DisabledComputers.Count.toString() + " newly disabled computer objects were found.")
155 |
156 | ForEach ($DisabledComputer in $DisabledComputers){
157 | #Moves the Computer.
158 | Write-Output ("Moving computer: " + $DisabledComputer.SamAccountName + "...")
159 | If ($ReportOnly) {
160 | Move-ADObject -Identity $DisabledComputer.DistinguishedName -TargetPath "OU=0-30 Days,OU=Computers,$ParentOU" -WhatIf
161 | }
162 | Else {
163 | Move-ADObject -Identity $DisabledComputer.DistinguishedName -TargetPath "OU=0-30 Days,OU=Computers,$ParentOU"
164 | }
165 | $MovedComputers++
166 |
167 | #Sets the description (notes) field with the old OU.
168 | #Formats the new description field (notes). Retains old Description if any exists.
169 | Write-Output ("Setting Description field for computer: " + $DisabledComputer.SamAccountName + "...")
170 | #Gets the parent OU.
171 | $OU = $DisabledComputer.DistinguishedName.Split(',',2)[1]
172 | If ([string]::IsNullOrWhiteSpace($DisabledComputer.Description)){
173 | $NewDescription = "OLD OU: " + $OU
174 | }
175 | Else{
176 | $NewDescription = "OLD OU: " + $OU + " | " + $DisabledComputer.Description
177 | }
178 | If ($ReportOnly){
179 | Set-ADComputer -Identity $DisabledComputer.SamAccountName -Replace @{Description=$NewDescription} -WhatIf
180 | }
181 | Else {
182 | Set-ADComputer -Identity $DisabledComputer.SamAccountName -Replace @{Description=$NewDescription}
183 | }
184 |
185 | #Sets ExtensionAttribute3 for the date of disablement.
186 | If ($DisabledComputer.ExtensionAttribute3 -notlike "INACTIVE*"){
187 | $Date = "DISABLED ON " + (Get-Date)
188 | Write-Output ("Setting ExtensionAttribute3 for computer: " + $DisabledComputer.SamAccountName + "...")
189 | If ($ReportOnly) {
190 | Set-ADComputer -Identity $DisabledComputer.SamAccountName -Replace @{ExtensionAttribute3=$Date} -WhatIf
191 | }
192 | Else {
193 | Set-ADComputer -Identity $DisabledComputer.SamAccountName -Replace @{ExtensionAttribute3=$Date}
194 | }
195 | }
196 | }
197 |
198 | #INCREMENT THROUGH OUS BASED ON AGE
199 | Write-Host ""
200 | Write-Output "OU INCREMENTATION:"
201 | #Gets objects in the disabled OU
202 | $DisabledOUUsers = Get-ADUser -Filter * -SearchBase $ParentOU -Properties ExtensionAttribute3,DistinguishedName,SamAccountName,Enabled | Where-Object {!$_.Enabled}
203 | $DisabledOUComputers = Get-ADComputer -Filter * -SearchBase $ParentOU -Properties ExtensionAttribute3,DistinguishedName,SamAccountName,Enabled | Where-Object {!$_.Enabled}
204 |
205 | #USERS
206 | #Declares counts for output.
207 | $30to180DayMovedUsers = 0
208 | $180DayMovedUsers = 0
209 | $180DayDeletedUsers = 0
210 |
211 | #Loops through users and checks if older than 30 days.
212 | ForEach ($DisabledOUUser in $DisabledOUUsers){
213 | #Sets the date disabled if not already set
214 | If ([string]::IsNullOrWhiteSpace($DisabledOUUser.ExtensionAttribute3) -or $DisabledOUUser.ExtensionAttribute3 -like "RE-ENABLED*"){
215 | Write-Output ("Setting ExtensionAttribute 3 for user: " + $DisabledOUUser.SamAccountName + " (user was in disabled objects OU but did not have a disabled date.)")
216 |
217 | $Date = "DISABLED ON " + (Get-Date)
218 | If ($ReportOnly){
219 | Set-ADUser -Identity $DisabledOUUser.SamAccountName -Replace @{ExtensionAttribute3=$Date} -WhatIf
220 | }
221 | Else {
222 | Set-ADUser -Identity $DisabledOUUser.SamAccountName -Replace @{ExtensionAttribute3=$Date}
223 | }
224 |
225 | #Sets the variable for comparison in the rest of the loop. It was null.
226 | $DisabledOUUser.ExtensionAttribute3 = $Date
227 | }
228 |
229 | #Extracts the date disabled for comparison.
230 | #If disabled by hand, count from the disable date
231 | If ($DisabledOUUser.ExtensionAttribute3 -like "DISABLED ON*"){
232 | $DateDisabled = [datetime]($DisabledOUUser.ExtensionAttribute3.Replace('DISABLED ON ',''))
233 | $DaysDisabled = (New-TimeSpan -Start $DateDisabled -End (Get-Date)).Days
234 | }
235 | #If auto-disabled because of inactivity (Disable-InactiveADAccounts script), add extra days to account for that.
236 | ElseIf ($DisabledOUUser.ExtensionAttribute3 -like "INACTIVE SINCE*"){
237 | $DateDisabled = [datetime]($DisabledOUUser.ExtensionAttribute3.Replace('INACTIVE SINCE ',''))
238 | $DaysDisabled = (New-TimeSpan -Start $DateDisabled.AddDays($UserInactivityDays) -End (Get-Date)).Days
239 | }
240 | Else {Write-Error ($DisabledOUUser.SamAccountName + " has an invalid disable date in ExtensionAttribute3.")}
241 |
242 | #Increment through OUs
243 | If ($DaysDisabled -ge 30 -and $DaysDisabled -le 180 -and $DisabledOUUser.DistinguishedName -notlike "*OU=30-180 Days,OU=Users,$ParentOU"){
244 | Write-Output ("Moving user to the 30-180 Days OU: " + $DisabledOUUser.SamAccountName)
245 | If ($ReportOnly) {
246 | Move-ADObject -Identity $DisabledOUUser.DistinguishedName -TargetPath "OU=30-180 Days,OU=Users,$ParentOU" -WhatIf
247 | }
248 | Else {
249 | Move-ADObject -Identity $DisabledOUUser.DistinguishedName -TargetPath "OU=30-180 Days,OU=Users,$ParentOU"
250 | }
251 | $30to180DayMovedUsers++
252 | }
253 | Else {
254 | If (!$DeleteUsersAt180){
255 | If ($DaysDisabled -gt 180 -and $DisabledOUUser.DistinguishedName -notlike "*OU=Over 180 Days,OU=Users,$ParentOU"){
256 | Write-Output ("Moving user to the Over 180 Days OU: " + $DisabledOUUser.SamAccountName)
257 | If ($ReportOnly) {
258 | Move-ADObject -Identity $DisabledOUUser.DistinguishedName -TargetPath "OU=Over 180 Days,OU=Users,$ParentOU" -WhatIf
259 | }
260 | Else {
261 | Move-ADObject -Identity $DisabledOUUser.DistinguishedName -TargetPath "OU=Over 180 Days,OU=Users,$ParentOU"
262 | }
263 | $180DayMovedUsers++
264 | }
265 | }
266 | Else {
267 | If ($DaysDisabled -gt 180){
268 | Write-Output ("Deleting >180 day inactive user: " + $DisabledOUUser.SamAccountName)
269 | If ($ReportOnly) {
270 | Remove-ADObject $DisabledOUUser.DistinguishedName -WhatIf
271 | }
272 | Else {
273 | Remove-ADObject $DisabledOUUser.DistinguishedName -Recursive -Confirm:$False
274 | }
275 | $180DayDeletedUsers++
276 | }
277 | }
278 | }
279 | }
280 |
281 | #COMPUTERS
282 | #Declares counts for output.
283 | $30to180DayMovedComputers = 0
284 | $180DayMovedComputers = 0
285 | $180DayDeletedComputers = 0
286 |
287 |
288 | #Loops through computers and checks if older than 90 days.
289 | ForEach ($DisabledOUComputer in $DisabledOUComputers){
290 | #Sets the date disabled if not already set
291 | If ([string]::IsNullOrWhiteSpace($DisabledOUComputer.ExtensionAttribute3) -or $DisabledOUComputer.ExtensionAttribute3 -like "RE-ENABLED*"){
292 | Write-Output ("Setting ExtensionAttribute 3 for Computer: " + $DisabledOUComputer.SamAccountName + " (computer was in disabled objects OU but did not have a disabled date.)")
293 |
294 | $Date = "DISABLED ON " + (Get-Date)
295 | If ($ReportOnly){
296 | Set-ADComputer -Identity $DisabledOUComputer.SamAccountName -Replace @{ExtensionAttribute3=$Date} -WhatIf
297 | }
298 | Else {
299 | Set-ADComputer -Identity $DisabledOUComputer.SamAccountName -Replace @{ExtensionAttribute3=$Date}
300 | }
301 |
302 | #Sets the variable for comparison in the rest of the loop. It was null.
303 | $DisabledOUComputer.ExtensionAttribute3 = $Date
304 | }
305 |
306 | #Extracts the date disabled for comparison.
307 | #If disabled by hand, count from the disable date
308 | If ($DisabledOUComputer.ExtensionAttribute3 -like "DISABLED ON*"){
309 | $DateDisabled = [datetime]($DisabledOUComputer.ExtensionAttribute3.Replace('DISABLED ON ',''))
310 | $DaysDisabled = (New-TimeSpan -Start $DateDisabled -End (Get-Date)).Days
311 | }
312 | #If auto-disabled because of inactivity (Disable-InactiveADAccounts script), add extra days to account for that.
313 | ElseIf ($DisabledOUComputer.ExtensionAttribute3 -like "INACTIVE SINCE*"){
314 | $DateDisabled = [datetime]($DisabledOUComputer.ExtensionAttribute3.Replace('INACTIVE SINCE ',''))
315 | $DaysDisabled = (New-TimeSpan -Start $DateDisabled.AddDays($ComputerInactivityDays) -End (Get-Date)).Days
316 | }
317 | Else {Write-Error ($DisabledOUComputer.SamAccountName + " has an invalid disable date in ExtensionAttribute3.")}
318 |
319 | #Increment through OUs
320 | If ($DaysDisabled -ge 30 -and $DaysDisabled -le 180 -and $DisabledOUComputer.DistinguishedName -notlike "*OU=30-180 Days,OU=Computers,$ParentOU"){
321 | Write-Output ("Moving computer to the 30-180 Days OU: " + $DisabledOUComputer.SamAccountName)
322 | If ($ReportOnly) {
323 | Move-ADObject -Identity $DisabledOUComputer.DistinguishedName -TargetPath "OU=30-180 Days,OU=Computers,$ParentOU" -WhatIf
324 | }
325 | Else {
326 | Move-ADObject -Identity $DisabledOUComputer.DistinguishedName -TargetPath "OU=30-180 Days,OU=Computers,$ParentOU"
327 | }
328 | $30to180DayMovedComputers++
329 | }
330 | Else{
331 | If (!$DeleteComputersAt180){
332 | If ($DaysDisabled -gt 180 -and $DisabledOUComputer.DistinguishedName -notlike "*OU=Over 180 Days,OU=Computers,$ParentOU"){
333 | Write-Output ("Moving computer to the Over 180 Days OU: " + $DisabledOUComputer.SamAccountName)
334 | If ($ReportOnly) {
335 | Move-ADObject -Identity $DisabledOUComputer.DistinguishedName -TargetPath "OU=Over 180 Days,OU=Computers,$ParentOU" -WhatIf
336 | }
337 | Else {
338 | Move-ADObject -Identity $DisabledOUComputer.DistinguishedName -TargetPath "OU=Over 180 Days,OU=Computers,$ParentOU"
339 | }
340 | $180DayMovedComputers++
341 | }
342 | }
343 | Else {
344 | If ($DaysDisabled -gt 180){
345 | Write-Output ("Deleting >180 day inactive computer: " + $DisabledOUcomputer.SamAccountName)
346 | If ($ReportOnly) {
347 | Remove-ADObject $DisabledOUcomputer.DistinguishedName -WhatIf
348 | }
349 | Else {
350 | Remove-ADObject $DisabledOUcomputer.DistinguishedName -Recursive -Confirm:$False
351 | }
352 | $180DayDeletedcomputers++
353 | }
354 | }
355 | }
356 | }
357 |
358 | #SUMMARY OUTPUT
359 | #Writes the counts of what was modified etc.
360 | Write-Output 'TOTALS:'
361 | $MovedUsers = $MovedUsers.tostring()
362 | Write-Output ($MovedUsers + ' user(s) moved to the "0-30 Days" OU.')
363 | $MovedComputers = $MovedComputers.tostring()
364 | Write-Output ($MovedComputers + ' computer(s) moved to the "0-30 Days" OU.')
365 | Write-Output ''
366 | $30to180DayMovedUsers = $30to180DayMovedUsers.tostring()
367 | Write-Output ($30to180DayMovedUsers + ' user(s) moved to the "30-180 Days" OU.')
368 | $30to180DayMovedComputers = $30to180DayMovedComputers.tostring()
369 | Write-Output ($30to180DayMovedComputers + ' computer(s) moved to the "30-180 Days" OU.')
370 | Write-Output ''
371 | If (!$DeleteUsersAt180){
372 | $180DayMovedUsers = $180DayMovedUsers.tostring()
373 | Write-Output ($180DayMovedUsers + ' user(s) moved to the "Over 180 Days" OU.')
374 | }
375 | Else {
376 | $180DayDeletedUsers = $180DayDeletedUsers.tostring()
377 | Write-Output ($180DayDeletedUsers + ' user(s) were DELETED for >180 days of inactivity.')
378 | }
379 | If (!$DeleteComputersAt180){
380 | $180DayMovedComputers = $180DayMovedComputers.tostring()
381 | Write-Output ($180DayMovedComputers + ' computer(s) moved to the "Over 180 Days" OU.')
382 | }
383 | Else {
384 | $180DayDeletedComputers = $180DayDeletedComputers.tostring()
385 | Write-Output ($180DayDeletedComputers + ' computer(s) were DELETED for >180 days of inactivity.')
386 | }
387 | Write-Output ''
388 |
389 | #ATTRIBUTE CLEANUP
390 | #Gets all AD objects with ExtensionAttribute3 incorrectly set, and clears it."
391 | Write-Output ""
392 | Write-Output "ATTRIBUTE CLEANUP:"
393 | Write-Output "Finding enabled users with incorrect ExtensionAttribute3 attributes..."
394 |
395 | $MalformedUsers = Get-ADUser -Filter * -Properties DistinguishedName,ExtensionAttribute3,SamAccountName,Enabled | Where-Object {
396 | $_.DistinguishedName -NotLike "*$ParentOU" -and
397 | $_.Enabled -eq $True -and
398 | ![string]::IsNullOrEmpty($_.ExtensionAttribute3) -and
399 | $_.ExtensionAttribute3 -notlike "DISABLED ON*" -and
400 | $_.ExtensionAttribute3 -notlike "INACTIVE SINCE*" -and
401 | $_.ExtensionAttribute3 -notlike "RE-ENABLED ON*"
402 | }
403 | $MalformedComputers = Get-ADComputer -Filter * -Properties DistinguishedName,ExtensionAttribute3,SamAccountName,Enabled | Where-Object {
404 | $_.DistinguishedName -NotLike "*$ParentOU" -and
405 | $_.Enabled -eq $True -and
406 | ![string]::IsNullOrEmpty($_.ExtensionAttribute3) -and
407 | $_.ExtensionAttribute3 -notlike "DISABLED ON*" -and
408 | $_.ExtensionAttribute3 -notlike "INACTIVE SINCE*" -and
409 | $_.ExtensionAttribute3 -notlike "RE-ENABLED ON*"
410 | }
411 |
412 | Write-Output (($MalformedUsers.SamAccountName.Count).ToString() + " users were found.")
413 | Write-Output (($MalformedComputers.SamAccountName.Count).ToString() + " computers were found.")
414 |
415 | ForEach ($MalformedUser in $MalformedUsers){
416 | Write-Output ("Clearing ExtensionAttribute3 for " + $MalformedUser.SamAccountName + "...")
417 | If ($ReportOnly){
418 | Set-ADUser -Identity $MalformedUser.SamAccountName -Clear ExtensionAttribute3 -WhatIf
419 | }
420 | Else {
421 | Set-ADUser -Identity $MalformedUser.SamAccountName -Clear ExtensionAttribute3
422 | }
423 | }
424 |
425 | ForEach ($MalformedComputer in $MalformedComputers){
426 | Write-Output ("Clearing ExtensionAttribute3 for " + $MalformedComputer.SamAccountName + "...")
427 | If ($ReportOnly){
428 | Set-ADComputer -Identity $MalformedComputer.SamAccountName -Clear ExtensionAttribute3 -WhatIf
429 | }
430 | Else {
431 | Set-ADComputer -Identity $MalformedComputer.SamAccountName -Clear ExtensionAttribute3
432 | }
433 | }
434 | }
435 |
436 | #LOGGING FUNCTION - starts transcript and cleans logs older than specified retention date.
437 | Function Start-Logging {
438 | <#
439 | .SYNOPSIS
440 | This function starts a transcript in the specified directory and cleans up any files older than the specified number of days.
441 |
442 | .DESCRIPTION
443 | Please ensure that the log directory specified is empty, as this function will clean that folder.
444 |
445 | .EXAMPLE
446 | Start-Logging -LogDirectory "C:\ScriptLogs\LogFolder" -LogName $LogName -LogRetentionDays 30
447 |
448 | .LINK
449 | https://github.com/AndrewEllis93/PowerShell-Scripts
450 |
451 | .NOTES
452 | Author: Andrew Ellis
453 | #>
454 | Param (
455 | [Parameter(Mandatory=$true)]
456 | [String]$LogDirectory,
457 | [Parameter(Mandatory=$true)]
458 | [String]$LogName,
459 | [Parameter(Mandatory=$true)]
460 | [Int]$LogRetentionDays
461 | )
462 |
463 | #Sets screen buffer from 120 width to 500 width. This stops truncation in the log.
464 | $ErrorActionPreference = 'SilentlyContinue'
465 | $pshost = Get-Host
466 | $pswindow = $pshost.UI.RawUI
467 |
468 | $newsize = $pswindow.BufferSize
469 | $newsize.Height = 3000
470 | $newsize.Width = 500
471 | $pswindow.BufferSize = $newsize
472 |
473 | $newsize = $pswindow.WindowSize
474 | $newsize.Height = 50
475 | $newsize.Width = 500
476 | $pswindow.WindowSize = $newsize
477 | $ErrorActionPreference = 'Continue'
478 |
479 | #Remove the trailing slash if present.
480 | If ($LogDirectory -like "*\") {
481 | $LogDirectory = $LogDirectory.SubString(0,($LogDirectory.Length-1))
482 | }
483 |
484 | #Create log directory if it does not exist already
485 | If (!(Test-Path $LogDirectory)) {
486 | New-Item -ItemType Directory $LogDirectory -Force | Out-Null
487 | }
488 |
489 | $Today = Get-Date -Format M-d-y
490 | Start-Transcript -Append -Path ($LogDirectory + "\" + $LogName + "." + $Today + ".log") | Out-Null
491 |
492 | #Shows proper date in log.
493 | Write-Output ("Start time: " + (Get-Date))
494 |
495 | #Purges log files older than X days
496 | $RetentionDate = (Get-Date).AddDays(-$LogRetentionDays)
497 | Get-ChildItem -Path $LogDirectory -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $RetentionDate -and $_.Name -like "*.log"} | Remove-Item -Force
498 | }
499 |
500 | #Start logging
501 | Start-Logging -LogDirectory "C:\ScriptLogs\Move-DisabledLog" -LogName "Move-DisabledLog" -LogRetentionDays 30
502 |
503 | #Call function
504 | Move-Disabled -ParentOU "OU=Disabled Objects,DC=domain,DC=local" -UserInactivityDays 30 -ComputerInactivityDays 60 -ReportOnly -ExclusionOUs @('OU=Test,DC=domain,DC=local','OU=Test2,DC=domain,DC=local') -ExclusionUserGroups @('ServiceAccts')
505 |
506 | #Stop logging.
507 | Write-Output ("Stop time: " + (Get-Date))
508 | Stop-Transcript
--------------------------------------------------------------------------------
/Move-StaleUserFolders.ps1:
--------------------------------------------------------------------------------
1 | ####################################################
2 | #
3 | # Title: Move-StaleUserFolders
4 | # Date Created : 2017-12-28
5 | # Last Edit: 2017-12-29
6 | # Author : Andrew Ellis
7 | # GitHub: https://github.com/AndrewEllis93/PowerShell-Scripts
8 | #
9 | # This script will scan all first-level sub-folders of the specified BasePath to find the most recent LastWriteTime in each one (recursively). This is really intended for user folders.
10 | # Any folder that does not contain any items modified over the threshold (in days) will be moved to the DisabledPath you specify.
11 | #
12 | ###########################################################
13 |
14 | Function Move-StaleUserFolders {
15 | <#
16 | .SYNOPSIS
17 | This script will scan all first-level sub-folders of the specified BasePath to find the most recent LastWriteTime in each one (recursively). This is really intended for user folders.
18 | Any folder that does not contain any items modified over the threshold (in days) will be moved to the DisabledPath you specify.
19 |
20 | .DESCRIPTION
21 |
22 | .EXAMPLE
23 | Move-StaleUserFolders -ReportOnly $True -BasePath "\\SERVER\users\" -DisablePath "\\SERVER\users\disable"
24 |
25 | .LINK
26 | https://github.com/AndrewEllis93/PowerShell-Scripts
27 |
28 | .NOTES
29 | Author: Andrew Ellis
30 | #>
31 |
32 | Param (
33 | [int]$Threshold = 180,
34 | [Parameter(Mandatory=$true)][string]$BasePath,
35 | [Parameter(Mandatory=$true)][string]$DisablePath,
36 | [bool]$ReportOnly = $False
37 | )
38 |
39 | #Remove the trailing slash if present.
40 | If ($BasePath -like "*\"){$BasePath = $BasePath.substring(0,($BasePath.Length-1))}
41 | If ($DisablePath -like "*\"){$DisablePath = $DisablePath.substring(0,($DisablePath.Length-1))}
42 |
43 | #Declarations
44 | $OldDirs = @()
45 | $ActiveDirs = @()
46 | $FailDirs = @()
47 |
48 | #Get all parent (home) folders
49 | $Dirs = Get-ChildItem $BasePath -Directory | Where-Object {$_.FullName -notlike "$DisablePath*"}
50 |
51 | $Dirs | ForEach-Object {
52 | $Fail = $False
53 |
54 | #Find the most recently modified item
55 | $FileTree = Get-ChildItem -Path $_.FullName -Recurse -Force | Sort-Object LastWriteTime -Descending
56 | #$SizeMB = ($FileTree | Measure-Object -property length -Sum).Sum / 1MB
57 | $LatestFile = $FileTree | Select-Object -First 1
58 |
59 | #Create object for output
60 | $FolderInfo = New-Object -TypeName PSObject
61 | $FolderInfo | Add-Member -MemberType NoteProperty -Name "ParentFolder" -Value $_.FullName
62 | $FolderInfo | Add-Member -MemberType NoteProperty -Name "LatestFile" -Value $LatestFile.FullName
63 | $FolderInfo | Add-Member -MemberType NoteProperty -Name "LastWriteTime" -Value $LatestFile.LastWriteTime
64 | #$FolderInfo | Add-Member -MemberType NoteProperty -Name "SizeMB" -Value $null
65 |
66 | #If there was no "most recently modified file", test the path.
67 | #If we can't access the path (access denied), it throws a warning.
68 | #If we CAN access the path, just set the last modified time to that of the parent folder.
69 | If (!$FolderInfo.LastWriteTime){
70 | If (Test-Path $_.FullName) {
71 | $FolderInfo.LastWriteTime = $_.LastWriteTime
72 | $FolderInfo.LatestFile = $_.FullName
73 | }
74 | Else {
75 | $Fail = $True
76 | $FolderInfo.LastWriteTime = $Null
77 | $FolderInfo.LatestFile = $Null
78 | }
79 | }
80 |
81 | #Console outputs and build arrays.
82 | If ($Fail) {
83 | Write-Warning ("WARNING: Unable to enumerate " + $FolderInfo.ParentFolder + ".")
84 | $FailDirs += $FolderInfo
85 | }
86 | If ($FolderInfo.LastWriteTime -and $FolderInfo.LastWriteTime -lt ((get-date).AddDays(($Threshold * -1)))){
87 | Write-Output ("Old directory found at " + $FolderInfo.ParentFolder + ". Last write time is " + $FolderInfo.LastWriteTime)
88 | $OldDirs += $FolderInfo
89 | }
90 | ElseIf ($FolderInfo.LastWriteTime) {
91 | Write-Output ("Active directory found at " + $FolderInfo.ParentFolder + ". Last write time is " + $FolderInfo.LastWriteTime)
92 | $ActiveDirs += $FolderInfo
93 | }
94 | }
95 |
96 | If (!$ReportOnly){
97 | #Sort by last modified, just for organization.
98 | #$FailDirs = $FailDirs | Sort-Object LastWriteTime
99 | #$OldDirs = $OldDirs | Sort-Object LastWriteTime
100 | #$ActiveDirs = $ActiveDirs | Sort-Object LastWriteTime
101 |
102 | #Loop through all old directories and move them to disable folder.
103 | $OldDirs | ForEach-Object {
104 | Write-Output ("Moving " + $_.ParentFolder + " to disable folder...")
105 | Move-Item $_.ParentFolder -Destination $Disablepath
106 | }
107 | }
108 | }
109 |
110 | Function Start-Logging {
111 | <#
112 | .SYNOPSIS
113 | This function starts a transcript in the specified directory and cleans up any files older than the specified number of days.
114 |
115 | .DESCRIPTION
116 | Please ensure that the log directory specified is empty, as this function will clean that folder.
117 |
118 | .EXAMPLE
119 | Start-Logging -LogDirectory "C:\ScriptLogs\LogFolder" -LogName $LogName -LogRetentionDays 30
120 |
121 | .LINK
122 | https://github.com/AndrewEllis93/PowerShell-Scripts
123 |
124 | .NOTES
125 | Author: Andrew Ellis
126 | #>
127 | Param (
128 | [Parameter(Mandatory=$true)]
129 | [String]$LogDirectory,
130 | [Parameter(Mandatory=$true)]
131 | [String]$LogName,
132 | [Parameter(Mandatory=$true)]
133 | [Int]$LogRetentionDays
134 | )
135 |
136 | #Sets screen buffer from 120 width to 500 width. This stops truncation in the log.
137 | $ErrorActionPreference = 'SilentlyContinue'
138 | $pshost = Get-Host
139 | $pswindow = $pshost.UI.RawUI
140 |
141 | $newsize = $pswindow.BufferSize
142 | $newsize.Height = 3000
143 | $newsize.Width = 500
144 | $pswindow.BufferSize = $newsize
145 |
146 | $newsize = $pswindow.WindowSize
147 | $newsize.Height = 50
148 | $newsize.Width = 500
149 | $pswindow.WindowSize = $newsize
150 | $ErrorActionPreference = 'Continue'
151 |
152 | #Remove the trailing slash if present.
153 | If ($LogDirectory -like "*\") {
154 | $LogDirectory = $LogDirectory.SubString(0,($LogDirectory.Length-1))
155 | }
156 |
157 | #Create log directory if it does not exist already
158 | If (!(Test-Path $LogDirectory)) {
159 | New-Item -ItemType Directory $LogDirectory -Force | Out-Null
160 | }
161 |
162 | $Today = Get-Date -Format M-d-y
163 | Start-Transcript -Append -Path ($LogDirectory + "\" + $LogName + "." + $Today + ".log") | Out-Null
164 |
165 | #Shows proper date in log.
166 | Write-Output ("Start time: " + (Get-Date))
167 |
168 | #Purges log files older than X days
169 | $RetentionDate = (Get-Date).AddDays(-$LogRetentionDays)
170 | Get-ChildItem -Path $LogDirectory -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $RetentionDate -and $_.Name -like "*.log"} | Remove-Item -Force
171 | }
172 |
173 | #Start logging.
174 | Start-Logging -LogDirectory "C:\ScriptLogs\Move-StaleUserFolders" -LogName "Move-StaleUserFolders" -LogRetentionDays 30
175 |
176 | #Start function.
177 | Move-StaleUserFolders -ReportOnly $True -BasePath "\\SERVER\users\" -DisablePath "\\SERVER\users\disable"
178 |
179 | #Stop logging.
180 | Stop-Transcript
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PowerShell Scripts
2 | Please read the header descriptions and comments in each script body, some contain important instructions or warnings.
3 |
4 | **ADHealthCheck:** This one is largely based on a script by Vikas Sukhija, who is credited in the body. I really only made some minor edits to his. Mine just adds a column that shows the last replication time and only emails if there is an unhealthy status or failure to cut down on email spam.
5 |
6 | **Disable-InactiveADAccounts:** Make sure you read through the comments (as with all of these scripts). It just finds the last logon for all AD accounts and disables any that have been inactive for X number of days (depending on what threshold you set). The difference with this script is that it gets the most accurate last logon available by comparing the results from all domain controllers. By default the lastlogontimestamp is only replicated every 14 days minus a random percentage of 5. This makes it much more accurate. It also supports AD exclusion groups (you can specify more than one) that allow you to exclude things like service accounts. It exports CSVs and sends an email report to the specified recipients.
7 |
8 | **Disable-InactiveADComputers** This is just a version of Disable-InactiveADAccounts for computer objects instead.
9 |
10 | **Discover-DriveSpace:** This one gets all your servers in AD and dumps the drives with sizes and remaining free space to a CSV (drivespace.csv). It also export some other files - pingable.txt, pingfail.txt, and servers.csv. Those should be self-explanatory.
11 |
12 | **Discover-Shares:** This one is a discovery function to find all Windows shares on the domain. Useful for acquisitions.
13 |
14 | **Dump-GPOs:** This one exports all of your GPOs' HTML reports, a CSV detailing all the GPO links, and a txt list of all the GPOs.
15 |
16 | **Enumerate-Access:** This function will spit back all of the permissions of a specified folder, recursively. You can choose to return inherited permissions or not. I wrote this specifically to show each and every ACL entry on a separate line. It's really useful for finding where a group or user is being used in NTFS ACLs. This helped us get rid of mail-enabled security groups by discovering each place that they were being used in NTFS ACLs so we could replace them. In most cases you won't want it to return inherited permissions (it doesn't by default) so you don't get a TON of redundant output, just the explicit ACL entries. It will generate a lot of disk activity on the target server because it scans the entire file system of the folder specified. At one point I actually combined this with the Find-Shares script to enumerate the ACLs on every file share we had. It took forever, needless to say, but helped a lot with weeding out old AD groups :)
17 |
18 | **Move Disabled:** This moves disabled users and computers, but instead of just moving them to a single OU, it rounds them up and ages them through different OUs (0-30 days, 30-180 days, over 180 days). It uses ExtensionAttribute3 to stamp the user/computer accounts with the disable date and notes the original OU in the description/info fields. Make sure you are NOT using ExtensionAttribute3 for anything else before running. Supports "-ReportOnly" argument (basically WhatIf). This is intended to be run daily. It also supports being used in conjunction with Disable-InactiveADAccounts.
19 |
20 | **Move-StaleUserFolders:** This script will scan all first-level sub-folders of the specified BasePath to find the most recent LastWriteTime in each one (recursively). This is really intended for user folders. It will move stale folders to the directory specified. You can modify this to just report instead, read the description up top.
21 |
22 | **Restart-DFSRAndEnableAutoRecovery:** Nice and short and simple. It restarts the DFSR service on all domain controllers (I schedule this to run nightly. This isn't really necessary but I have found it to prevent some misc issues that crop up once in a blue moon) and enables DFSR auto-recovery, which for whatever reason is disabled on domain controllers by default.
23 |
24 | **Send-PasswordNotices:** This sends password expiration notice emails to users at 1,2,3,7, and 14 days. Supports an AD exclusion group.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Restart-DFSRAndEnableAutoRecovery.ps1:
--------------------------------------------------------------------------------
1 | ####################################################
2 | #
3 | # Title: Restart-DFSRAndEnableAutoRecovery
4 | # Date Created : 2017-12-28
5 | # Last Edit: 2017-12-29
6 | # Author : Andrew Ellis
7 | # GitHub: https://github.com/AndrewEllis93/PowerShell-Scripts
8 | #
9 | # Nice and short and simple. It restarts the DFSR service on all domain controllers (I schedule this to run nightly. This isn't really necessary but I have found it to prevent some misc issues that crop up once in a blue moon) and enables DFSR auto-recovery, which for whatever reason is disabled on domain controllers by default.
10 | #
11 | ####################################################
12 |
13 | Function Restart-DFSRAndEnableAutoRecovery {
14 | <#
15 | .SYNOPSIS
16 | Gets all DCs, restarts the DFSR service, and enables DFSR auto recovery, which is turned off by default for who knows what reason.
17 |
18 | .DESCRIPTION
19 |
20 | .EXAMPLE
21 | Restart-DFSRAndEnableAutoRecovery
22 |
23 | .LINK
24 | https://github.com/AndrewEllis93/PowerShell-Scripts
25 |
26 | .NOTES
27 | Author: Andrew Ellis
28 | #>
29 |
30 | Write-Output "Getting list of DCs..."
31 | $DCs = Get-ADGroupMember 'Domain Controllers' -ErrorAction Stop
32 |
33 | ForEach ($DC in $DCs)
34 | {
35 | $Output = "Restarting DFSR service on " + $DC.Name + "..."
36 | Write-Output $Output
37 | Invoke-Command -ComputerName $DC.Name -ScriptBlock {Restart-Service DFSR}
38 | Start-Sleep 5
39 | Write-Output ("Enabling DFSR auto recovery on " + $DC.Name + "...")
40 | Invoke-Command -ComputerName $DC.Name -ScriptBlock {cmd.exe /c wmic /namespace:\\root\microsoftdfs path dfsrmachineconfig set StopReplicationOnAutoRecovery=FALSE}
41 | }
42 | }
43 | Function Start-Logging {
44 | <#
45 | .SYNOPSIS
46 | This function starts a transcript in the specified directory and cleans up any files older than the specified number of days.
47 |
48 | .DESCRIPTION
49 | Please ensure that the log directory specified is empty, as this function will clean that folder.
50 |
51 | .EXAMPLE
52 | Start-Logging -LogDirectory "C:\ScriptLogs\LogFolder" -LogName $LogName -LogRetentionDays 30
53 |
54 | .LINK
55 | https://github.com/AndrewEllis93/PowerShell-Scripts
56 |
57 | .NOTES
58 | Author: Andrew Ellis
59 | #>
60 | Param (
61 | [Parameter(Mandatory=$true)]
62 | [String]$LogDirectory,
63 | [Parameter(Mandatory=$true)]
64 | [String]$LogName,
65 | [Parameter(Mandatory=$true)]
66 | [Int]$LogRetentionDays
67 | )
68 |
69 | #Sets screen buffer from 120 width to 500 width. This stops truncation in the log.
70 | $ErrorActionPreference = 'SilentlyContinue'
71 | $pshost = Get-Host
72 | $pswindow = $pshost.UI.RawUI
73 |
74 | $newsize = $pswindow.BufferSize
75 | $newsize.Height = 3000
76 | $newsize.Width = 500
77 | $pswindow.BufferSize = $newsize
78 |
79 | $newsize = $pswindow.WindowSize
80 | $newsize.Height = 50
81 | $newsize.Width = 500
82 | $pswindow.WindowSize = $newsize
83 | $ErrorActionPreference = 'Continue'
84 |
85 | #Remove the trailing slash if present.
86 | If ($LogDirectory -like "*\") {
87 | $LogDirectory = $LogDirectory.SubString(0,($LogDirectory.Length-1))
88 | }
89 |
90 | #Create log directory if it does not exist already
91 | If (!(Test-Path $LogDirectory)) {
92 | New-Item -ItemType Directory $LogDirectory -Force | Out-Null
93 | }
94 |
95 | $Today = Get-Date -Format M-d-y
96 | Start-Transcript -Append -Path ($LogDirectory + "\" + $LogName + "." + $Today + ".log") | Out-Null
97 |
98 | #Shows proper date in log.
99 | Write-Output ("Start time: " + (Get-Date))
100 |
101 | #Purges log files older than X days
102 | $RetentionDate = (Get-Date).AddDays(-$LogRetentionDays)
103 | Get-ChildItem -Path $LogDirectory -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $RetentionDate -and $_.Name -like "*.log"} | Remove-Item -Force
104 | }
105 |
106 | #Start logging.
107 | Start-Logging -logdirectory "C:\ScriptLogs\Restart-DFSRAndEnableAutoRecovery" -logname "Restart-DFSRAndEnableAutoRecovery" -LogRetentionDays 30
108 |
109 | #Start function.
110 | Restart-DFSRAndEnableAutoRecovery
111 |
112 | #Stops logging.
113 | Stop-Transcript
114 |
--------------------------------------------------------------------------------
/Send-PasswordNotices.ps1:
--------------------------------------------------------------------------------
1 | ####################################################
2 | #
3 | # Title: Send-PasswordNotices
4 | # Date Created : 2017-05-01
5 | # Last Edit: 2017-12-29
6 | # Author : Andrew Ellis
7 | # GitHub: https://github.com/AndrewEllis93/PowerShell-Scripts
8 | #
9 | # This sends password expiration notice emails to users at 1,2,3,7, and 14 days. Supports an AD exclusion group.
10 | # Comment out this line starting with "Send-MailMessage" to just get output without actuall sending any email.
11 | #
12 | ####################################################
13 |
14 | Function Start-Logging {
15 | <#
16 | .SYNOPSIS
17 | This function starts a transcript in the specified directory and cleans up any files older than the specified number of days.
18 |
19 | .DESCRIPTION
20 | Please ensure that the log directory specified is empty, as this function will clean that folder.
21 |
22 | .EXAMPLE
23 | Start-Logging -LogDirectory "C:\ScriptLogs\LogFolder" -LogName $LogName -LogRetentionDays 30
24 |
25 | .LINK
26 | https://github.com/AndrewEllis93/PowerShell-Scripts
27 |
28 | .NOTES
29 | Author: Andrew Ellis
30 | #>
31 | Param (
32 | [Parameter(Mandatory=$true)]
33 | [String]$LogDirectory,
34 | [Parameter(Mandatory=$true)]
35 | [String]$LogName,
36 | [Parameter(Mandatory=$true)]
37 | [Int]$LogRetentionDays
38 | )
39 |
40 | #Sets screen buffer from 120 width to 500 width. This stops truncation in the log.
41 | $ErrorActionPreference = 'SilentlyContinue'
42 | $pshost = Get-Host
43 | $pswindow = $pshost.UI.RawUI
44 |
45 | $newsize = $pswindow.BufferSize
46 | $newsize.Height = 3000
47 | $newsize.Width = 500
48 | $pswindow.BufferSize = $newsize
49 |
50 | $newsize = $pswindow.WindowSize
51 | $newsize.Height = 50
52 | $newsize.Width = 500
53 | $pswindow.WindowSize = $newsize
54 | $ErrorActionPreference = 'Continue'
55 |
56 | #Remove the trailing slash if present.
57 | If ($LogDirectory -like "*\") {
58 | $LogDirectory = $LogDirectory.SubString(0,($LogDirectory.Length-1))
59 | }
60 |
61 | #Create log directory if it does not exist already
62 | If (!(Test-Path $LogDirectory)) {
63 | New-Item -ItemType Directory $LogDirectory -Force | Out-Null
64 | }
65 |
66 | $Today = Get-Date -Format M-d-y
67 | Start-Transcript -Append -Path ($LogDirectory + "\" + $LogName + "." + $Today + ".log") | Out-Null
68 |
69 | #Shows proper date in log.
70 | Write-Output ("Start time: " + (Get-Date))
71 |
72 | #Purges log files older than X days
73 | $RetentionDate = (Get-Date).AddDays(-$LogRetentionDays)
74 | Get-ChildItem -Path $LogDirectory -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $RetentionDate -and $_.Name -like "*.log"} | Remove-Item -Force
75 | }
76 |
77 | Function Send-Notice
78 | {
79 | <#
80 | .SYNOPSIS
81 | Customizes and sends an email message and subject based on the number of days left before password expiry.
82 |
83 | .DESCRIPTION
84 | Send-notice - sends emails to users based on days before password expiration. Requires user email address, days before password expiration, password expiration date, and user account name variables.
85 | Notices are only sent if days before password is due to expire are equal to 1,2,3,7, or 14.
86 |
87 | .LINK
88 | https://github.com/AndrewEllis93/PowerShell-Scripts
89 |
90 | .NOTES
91 | Author: Andrew Ellis
92 | #>
93 |
94 | param(
95 | [Parameter(Mandatory=$True)][string]$usermail,
96 | [Parameter(Mandatory=$True)][Int]$days,
97 | [Parameter(Mandatory=$True)][datetime]$expirationdate,
98 | [Parameter(Mandatory=$True)][string]$SAM,
99 | [Parameter(Mandatory=$True)][string]$SMTPServer,
100 | [Parameter(Mandatory=$True)][string]$MailFrom
101 | )
102 |
103 | If (@(0,1) -contains $Days)
104 | {
105 | $SendNotice = $True
106 | $subject = "FINAL PASSWORD CHANGE NOTIFICATION - Your network password will expire in less than 24 hours."
107 | $body = "----Final Password Change Notice----`n`n"
108 | $body += "Your network password is due to expire within the next 24 hours.`n`n"
109 | write-output ("$days Day Notice sent to $SAM. Password expiration date: $expirationdate")
110 | }
111 | ElseIf (@(2,3,7,14) -contains $Days)
112 | {
113 | $SendNotice = $True
114 | $subject = "PASSWORD CHANGE NOTIFICATION - Your network password will expire in $days days."
115 | $body = "----$days Day Password Change Notice----`n`n"
116 | $body += "Your network password is due to expire in $days days.`n`n"
117 | write-output ("$days Day Notice sent to $SAM. Password expiration date: $expirationdate (in $days days)")
118 | }
119 |
120 | If ($SendNotice)
121 | {
122 | $body += "Please change your password before the expiration date to ensure you do not lose network access due to an expired password. `n`n"
123 | $body += "`n`n"
124 | $body += "To change your password, please close all open programs and press Ctrl-Alt-Del then choose `"Change Password`" from the list. `n`n"
125 | $body += "If you are unable to change your password, please contact the Help Desk. `n`n"
126 | $body += "*This is an automated message, please do not reply. Any replies will not be delivered.* `n`n"
127 |
128 | Send-MailMessage -To $usermail -From $mailfrom -Subject $subject -Body $body -SmtpServer $smtpserver
129 | }
130 | Else
131 | {
132 | #Write-output ("Notice not sent to $SAM. Password expiration date: $expirationdate (in $days days)")
133 | }
134 | }
135 |
136 | Function Send-AllNotices {
137 | <#
138 | .SYNOPSIS
139 | Main process. Collects user accounts, calculates password expiration dates and passes the value along with user information to the send-notice function.
140 |
141 | .DESCRIPTION
142 |
143 | .EXAMPLE
144 | Send-AllNotices -ADGroupExclusion "Test Group" -MailFrom "noreply@email.com" -smtpserver "server.domain.local"
145 |
146 | .LINK
147 | https://github.com/AndrewEllis93/PowerShell-Scripts
148 |
149 | .NOTES
150 | Author: Andrew Ellis
151 | #>
152 |
153 | Param (
154 | [string]$ADGroupExclusion,
155 | [Parameter(Mandatory=$true)][string]$MailFrom,
156 | [Parameter(Mandatory=$true)][string]$smtpserver
157 | )
158 |
159 | $ServiceAccounts = Get-ADGroupMember -Identity $ADGroupExclusion -ErrorAction Stop
160 | $Users = Get-ADUser -Filter {(enabled -eq $true -and passwordneverexpires -eq $false)} -properties samaccountname, name, mail, msDS-UserPasswordExpiryTimeComputed -ErrorAction Stop |
161 | Select-Object samaccountname, name, mail, msDS-UserPasswordExpiryTimeComputed
162 |
163 | #Filter users
164 | If ($ADGroupExclusion){
165 | $Users = $Users | Where-Object {
166 | $_.'msDS-UserPasswordExpiryTimeComputed' -and
167 | $_.Mail -and $_.SamAccountName -and
168 | $ServiceAccounts.SamAccountName -notcontains $_.SamAccountName
169 | } | Sort-Object -Property 'msDS-UserPasswordExpiryTimeComputed'
170 | }
171 | Else {
172 | $Users = $Users | Where-Object {
173 | $_.'msDS-UserPasswordExpiryTimeComputed' -and
174 | $_.Mail -and $_.SamAccountName
175 | } | Sort-Object -Property 'msDS-UserPasswordExpiryTimeComputed'
176 | }
177 |
178 | #Loop through users and send notices
179 | $Users | foreach-object {
180 | $Expirationdate = [datetime]::FromFileTime($_.'msDS-UserPasswordExpiryTimeComputed')
181 | $Expirationdays = ($Expirationdate - (Get-Date)).Days
182 |
183 | Send-Notice -usermail $_.Mail -days $ExpirationDays -expirationdate $expirationdate -SAM $_.SamAccountName -SMTPServer $smtpserver -MailFrom $mailfrom
184 | }
185 | }
186 |
187 | #Start logging.
188 | Start-Logging -logdirectory "C:\ScriptLogs\SendPasswordNotices" -logname "SendPasswordNotices" -LogRetentionDays 30
189 |
190 | #Start function
191 | Send-AllNotices -ADGroupExclusion "Test Group" -MailFrom "noreply@email.com" -smtpserver "server.domain.local"
192 |
193 | #Stop logging.
194 | Stop-Transcript
195 |
--------------------------------------------------------------------------------