├── .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 "" 129 | add-content $report "" 130 | add-content $report "
" 127 | add-content $report "Active Directory Health Check" 128 | add-content $report "
" 131 | 132 | add-content $report "" 133 | Add-Content $report "" 134 | Add-Content $report "" 135 | Add-Content $report "" 136 | Add-Content $report "" 137 | Add-Content $report "" 138 | Add-Content $report "" 139 | Add-Content $report "" 140 | Add-Content $report "" 141 | Add-Content $report "" 142 | Add-Content $report "" 143 | Add-Content $report "" 144 | Add-Content $report "" 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 "" 210 | Add-Content $report "" 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 "" 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 "" 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 "" 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 "" 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 "" 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 "" 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 "" 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 "" 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 "" 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 "" 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 "" 313 | } 314 | else 315 | { 316 | Write-Host $DC `t Netlogons Test Failed -ForegroundColor Red 317 | Add-Content $report "" 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 "" 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 "" 339 | } 340 | else 341 | { 342 | Write-Host $DC `t Replications Test Failed -ForegroundColor Red 343 | Add-Content $report "" 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 "" 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 "" 365 | } 366 | else 367 | { 368 | Write-Host $DC `t Services Test Failed -ForegroundColor Red 369 | Add-Content $report "" 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 "" 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 "" 391 | } 392 | else 393 | { 394 | Write-Host $DC `t Advertising Test Failed -ForegroundColor Red 395 | Add-Content $report "" 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 "" 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 "" 417 | } 418 | else 419 | { 420 | Write-Host $DC `t FSMOCheck Test Failed -ForegroundColor Red 421 | Add-Content $report "" 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 "" 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 "" 444 | } 445 | else { 446 | Write-Host $DC `t DFSR Last Rep Test Failed -ForegroundColor Red 447 | Add-Content $report "" 448 | } 449 | } 450 | } 451 | else { 452 | Write-Host $DC `t $DC `t Ping Fail -ForegroundColor Red 453 | Add-Content $report "" 454 | Add-Content $report "" 455 | Add-Content $report "" 456 | Add-Content $report "" 457 | Add-Content $report "" 458 | Add-Content $report "" 459 | Add-Content $report "" 460 | Add-Content $report "" 461 | Add-Content $report "" 462 | Add-Content $report "" 463 | } 464 | } 465 | 466 | Add-Content $report "" 467 | ############################################Close HTMl Tables########################### 468 | 469 | 470 | Add-content $report "
IdentityPingStatusNetlogonServiceNTDSServiceDNSServiceStatusNetlogonsTestReplicationTestServicesTestAdvertisingTestFSMOCheckTestDfsrLastRepTest
$ShortIdentity SuccessTimeout$svcState$svcStateTimeout$svcState$svcStateTimeout$svcState$svcStateTimeoutPassFailTimeoutPassFailTimeoutPassFailTimeoutPassFailTimeoutPassFailTimeout$DfsrLastUpdateTestResults$DfsrLastUpdateTestResults $Identity Ping Fail Ping Fail Ping Fail Ping Fail Ping Fail Ping Fail Ping Fail Ping Fail Ping Fail
" 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 | --------------------------------------------------------------------------------