Using Powershell and the ADSI functions of this opened the doors to the completion of the task.
$users = $([ADSI]"WinNT://$env:COMPUTERNAME").Children | where {$_.SchemaClassName -eq 'user'}
The above command would output all the local users of the server, and then adding an array to remove certain service accounts was the next part because we still needed a backup account (the local administrator) and the services for monitoring and antivirus
$svc = @("Administrator","_svc-monitoring","SophosSAUWIN")
It was easy to omit those from the list using a simple ForEach loop
foreach ($s in $svc) {
$users = $users | where {$_.Name -ne "$s"}
}
In the early testing, it would error out if there was a user account that was setup, and had never logged in. For this, I broke it up into two arrays; an array of active users (those who had logged in) and an array of inactive users (those who had never logged in). Fairly simple stuff ...
$active = @()
foreach ($u in $users){
$activeUsers = @{Name = $u.Name} | where {$u.Properties.LastLogin.Value -ne $null}
$activeObj = New-Object PSObject –Property $activeUsers
$active += $activeUsers
}
$inactive = @()
foreach ($u in $users){
$inactiveUsers = @{Name = $u.Name} | where {$u.Properties.LastLogin.Value -eq $null}
$inactiveObj = New-Object PSObject –Property $inactiveUsers
$inactive += $inactiveUsers
}
Next step was to set some disable parameters. The first two are straight forward in getting the date the script runs, used later for the user description to show when the account was disabled, and the other to subtract 90 days and convert to a large integer
$today = Get-Date
$nintyDays = ((Get-Date).AddDays(-90)).TofileTime()
This one was found in a TechNet article stating all the user flags, basically it takes whatever the userflag is and adds two. Simple enough. So if the default user that is setup has a flag of 513, than 515 would be disabled.
$ADS_UF_ACCOUNTDISABLE = 0x0002
The brunt of the work happens in the following two ForEach loops. The first looping thorough accounts that have actually logged in, and the second looping through accounts that have not logged in. This actually works out better, because I figure if someone requests an account and doesn't log in than why even create the account?
In the arrays created above, all I am collecting there are the usernames to pass in as a variable into the ADSI string saved as the $objUser variable which then grabs all the information that is tied to the user account. The if statement grabs the user flags and adds the disable account flag. This is because if the account was disabled a week ago, I wanted to maintain the account description on the date that it was disabled. Without that it would keep overwriting the description with the date that the script ran. I put the Write-Host in there for the purposes of testing, but the else statement will disable the account and change the description.
foreach ($a in $active) {
$username = $a.Name
$objUser = [ADSI]"WinNT://WORKGROUP/$env:COMPUTERNAME/$username"
if ($objUser.psbase.Properties.item("userflags").value -band $ADS_UF_ACCOUNTDISABLE) {
Write-Host "$username already disabled"}
else {
$logon = ($objUser.LastLogin).ToFileTime()
if ($logon -le $nintyDays){
$objUser.description = "Account Disabled $today"
$objUser.userflags = $disableUser
$objUser.setinfo()}
}
}
foreach ($i in $inactive) {
$username = $i.Name
$objUser = [ADSI]"WinNT://WORKGROUP/$env:COMPUTERNAME/$username"
if ($objUser.psbase.properties.item("userflags").value -band $ADS_UF_ACCOUNTDISABLE) {
Write-Host "$username already disabled"}
else {
$objUser.description = "Account Disabled $today"
$objUser.userflags = $disableUser
$objUser.setinfo()
}
}
Still with me? That was pretty easy. The next step was to make it run every morning at 1:30am with a scheduled task. The only way I could figure that out was to have a batch script, because the script needs to run as admin. The bat file is listed below.
@echo off
set scriptFileName=disableUser
set scriptFolderPath=c:\tools\scripts
set powershellScriptFileName=%scriptFileName%.ps1
powershell -Command "Start-Process powershell \"-ExecutionPolicy Bypass -NoProfile -NoExit -Command `\"cd \`\"%scriptFolderPath%\`\"; & \`\".\%powershellScriptFileName%\`\"`\"\" -Verb RunAs"
Here's the xml of the scheduled task for reference, but is probably the easiest part of this task to setup, but could be imported fairly quickly if needed.
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>2018-04-18T12:34:56.0000000</Date>
<Author>WIN-JUMPPY\Al.Fredo</Author>
</RegistrationInfo>
<Triggers>
<CalendarTrigger>
<StartBoundary>2018-04-18T01:30:00</StartBoundary>
<Enabled>false</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>WIN-JUMPPY\Al.Fredo</UserId>
<LogonType>Password</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>P3D</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>C:\Tools\scripts\disableUser.bat</Command>
<WorkingDirectory>C:\Tools\scripts</WorkingDirectory>
</Exec>
</Actions>
</Task>
That's mostly it. If there are any questions, feel free to let me know! Just a side note though, in Server 2016 there is a Powershell module that can handle this. Thanks for reading.