Active Directory
banshee/active-directory-collection/active-directory/winrm/
service-accounts
The days left before the password of each service account of a domain expires, by WinRM, PowerShell and LDAP
by banshee1 installation2 versions
v2Latest
Published on October 28, 2026. One naming convention across collections; documentation completed.
// The passwords of the service accounts of a domain that expire, by WinRM: for each enabled user account that
// carries a service principal name (servicePrincipalName), the days left before its password expires. A
// service whose password expires stops at its next logon.
// Runs one PowerShell script that only reads the directory over LDAP, on the domain controller itself: the
// enabled users (bit 0x2, ACCOUNTDISABLE, of userAccountControl unset) with a servicePrincipalName, the krbtgt
// accounts left out; then, for each one whose password may expire (bit 0x10000, DONT_EXPIRE_PASSWORD, unset),
// msDS-UserPasswordExpiryTimeComputed, which the directory computes from the password policy of the domain or
// of the password settings of the user. Group managed service accounts are not users: Windows changes their
// password itself.
// A service account is a choice: the probe narrows the selection to "Only" the accounts to watch.
// The script is the one of active-directory/ssh/service-accounts, without the powershell -Command "..." that
// cmd.exe needs there.
need('protocol:winrm');
per('account', 'Accounts');
output('expires', 'text', array('label' => 'Password expires', 'per' => 'account'));
output('days-left', 'number', array('label' => 'Before the password expires', 'unit' => 'd', 'per' => 'account'));
watch.below('expiring', 'Password expiring', 'days-left', array('warning' => 14, 'critical' => 3));
watch.missing('missing', 'Service account missing', 'expires', array('enabled' => false));
$read = winrm.run("$ErrorActionPreference = 'Stop'; "
. "[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding; "
. "$computer = Get-CimInstance Win32_ComputerSystem; "
. "$server = $computer.DNSHostName + '.' + $computer.Domain; "
. "$rootDSE = [ADSI]('LDAP://' + $server + '/RootDSE'); "
. "$domain = [ADSI]('LDAP://' + $server + '/' + $rootDSE.Properties['defaultNamingContext'][0]); "
. "$searcher = New-Object DirectoryServices.DirectorySearcher($domain,"
. " '(&(objectCategory=person)(objectClass=user)(servicePrincipalName=*)(!(sAMAccountName=krbtgt*))"
. "(!(userAccountControl:1.2.840.113556.1.4.803:=2)))', @('sAMAccountName', 'userAccountControl')); "
. "$searcher.PageSize = 1000; "
. "$never = [DateTime]::MaxValue.ToFileTimeUtc(); "
. "$now = [DateTime]::UtcNow; "
. "$accounts = @($searcher.FindAll() | ForEach-Object {"
. " $account = @{name = [string]$_.Properties['samaccountname'][0]; expires = $false; days = 0};"
. " if (([long]$_.Properties['useraccountcontrol'][0] -band 65536) -eq 0) {"
. " $base = New-Object DirectoryServices.DirectorySearcher($_.GetDirectoryEntry(), '(objectClass=*)',"
. " @('msDS-UserPasswordExpiryTimeComputed'), 'Base');"
. " $time = [long]$base.FindOne().Properties['msds-userpasswordexpirytimecomputed'][0];"
. " if ($time -lt $never) { $account.expires = $true;"
. " $account.days = [long]([DateTime]::FromFileTimeUtc($time) - $now).TotalDays } };"
. " $account }); "
. "ConvertTo-Json -Compress -Depth 4 -InputObject @{accounts = $accounts}");
$answer = text.trim($read['output']);
if (list.count(text.match($answer, '^\{')) == 0) {
fail('misconfigured', 'PowerShell did not return the service accounts', $answer . $read['errors']);
}
$service_accounts = json.read($answer);
$accounts = 0;
foreach ($service_accounts['accounts'] as $account) {
$accounts = $accounts + 1;
if ($account['expires']) {
value('expires', 'yes', $account['name']);
value('days-left', $account['days'], $account['name']);
} else {
value('expires', 'no', $account['name']);
}
}
if ($accounts == 0) {
fail('misconfigured', 'No enabled user account with a service principal name in the domain', $answer);
}