Skip to content
ConsoleMonster

Stale Active Directory accounts

List the accounts nobody uses, then lay the month's password expiries and the disable date on a calendar.

Demos Reports

Use it when the quarterly access review needs the inactive accounts and the next month's expiries in one place.

  • Static
  • 100×26
  • PowerShell 7.4+
  • .NET 8.0+
Stale Active Directory accounts 100×2624-bitFull

How it's built

The whole program in one file, with its sample data at the top. It drew the recording above; all three styles produce the same frame bytes.

PowerShellstale-accounts.ps1View on GitHub
# Find stale Active Directory accounts and see which passwords expire this month.
Import-Module -Name ConsoleMonster

# Data: fictional accounts and a fixed report date; no domain controller is queried.
$today = [datetime]'2026-11-02'
$accounts = @(
    [pscustomobject] @{ Account = 'amy.lee'; Department = 'Finance'
        LastLogon = [datetime]'2026-10-30'; PasswordSet = [datetime]'2026-08-20'
        PasswordExpires = [datetime]'2026-11-18' }
    [pscustomobject] @{ Account = 'ben.ortiz'; Department = 'IT'; LastLogon = [datetime]'2026-11-01'
        PasswordSet = [datetime]'2026-08-06'; PasswordExpires = [datetime]'2026-11-04' }
    [pscustomobject] @{ Account = 'svc-backup'; Department = 'Service'
        LastLogon = [datetime]'2026-11-02'; PasswordSet = [datetime]'2025-03-01'
        PasswordExpires = $null }
    [pscustomobject] @{ Account = 'cara.diaz'; Department = 'HR'; LastLogon = [datetime]'2026-07-12'
        PasswordSet = [datetime]'2026-05-30'; PasswordExpires = [datetime]'2026-08-28' }
    [pscustomobject] @{ Account = 'dan.reyes'; Department = 'Sales'
        LastLogon = [datetime]'2026-08-25'; PasswordSet = [datetime]'2026-08-11'
        PasswordExpires = [datetime]'2026-11-09' }
    [pscustomobject] @{ Account = 'contractor-07'; Department = 'External'
        LastLogon = [datetime]'2026-05-02'; PasswordSet = [datetime]'2026-04-20'
        PasswordExpires = [datetime]'2026-07-19' }
    [pscustomobject] @{ Account = 'eve.chen'; Department = 'Marketing'
        LastLogon = [datetime]'2026-10-28'; PasswordSet = [datetime]'2026-08-14'
        PasswordExpires = [datetime]'2026-11-12' }
    [pscustomobject] @{ Account = 'finn.park'; Department = 'IT'; LastLogon = [datetime]'2026-10-31'
        PasswordSet = [datetime]'2026-08-28'; PasswordExpires = [datetime]'2026-11-26' }
    [pscustomobject] @{ Account = 'gia.rossi'; Department = 'Finance'
        LastLogon = [datetime]'2026-09-01'; PasswordSet = [datetime]'2026-08-24'
        PasswordExpires = [datetime]'2026-11-22' }
    [pscustomobject] @{ Account = 'training-01'; Department = 'Shared'
        LastLogon = [datetime]'2026-02-14'; PasswordSet = [datetime]'2025-11-03'
        PasswordExpires = $null }
    [pscustomobject] @{ Account = 'hal.novak'; Department = 'Legal'
        LastLogon = [datetime]'2026-11-02'; PasswordSet = [datetime]'2026-08-10'
        PasswordExpires = [datetime]'2026-11-08' }
)

# Behaviour: stale means no logon for 60 days or a password older than a year.
$stale = foreach ($account in $accounts) {
    $inactive = ($today - $account.LastLogon).Days
    $age = ($today - $account.PasswordSet).Days
    if ($inactive -gt 60 -or $age -gt 365) {
        $status = if ($inactive -gt 90 -or $age -gt 365) { 'Fail' } else { 'Warn' }
        [pscustomobject] @{ Account = $account.Account; Department = $account.Department
            LastLogon = $account.LastLogon.ToString('yyyy-MM-dd'); Inactive = $inactive
            PasswordAge = $age; Status = $status }
    }
}
$expiring = @($accounts |
    Where-Object -FilterScript {
        $_.PasswordExpires -ge $today -and $_.PasswordExpires -le $today.AddDays(30)
    } |
    Sort-Object -Property PasswordExpires)
$marks = @(
    [pscustomobject] @{ Date = [datetime]'2026-11-16'; Status = 'Fail'
        Label = "Disable $(@($stale).Count) accounts" }
    foreach ($account in $expiring) {
        $status = if ($account.PasswordExpires -le $today.AddDays(7)) { 'Warn' } else { 'Info' }
        [pscustomobject] @{ Date = $account.PasswordExpires; Status = $status
            Label = "$($account.Account) password" }
    }
) | Sort-Object -Property Date

# Layout: the accounts to act on, then the month of expiries beside the dated list.
$report = New-ConsoleRows -Gap 1 -Content {
    New-ConsoleRule -Title '[bold]Account hygiene[/] [muted]· adatum.example.test · as of 2026-11-02[/]'
    New-ConsoleTable -InputObject $stale -Property Account, Department, LastLogon, Inactive,
        PasswordAge, Status -Expand
    New-ConsoleColumns -Content {
        New-ConsolePanel -Title 'November' -Content {
            $marks | New-ConsoleCalendar -Year 2026 -Month 11 -FirstDayOfWeek Monday -Today $today
        }
        New-ConsolePanel -Title 'Coming up' -Content {
            New-ConsoleStatusList -Content {
                foreach ($mark in $marks) {
                    [pscustomobject] @{ Label = $mark.Date.ToString('ddd dd MMM',
                        [cultureinfo]'en-US')
                        Status = $mark.Status; Detail = $mark.Label }
                }
            }
        }
    }
}

# Behaviour: print once; confirm each stale account with its manager before disabling it.
$report | Show-Console
C#Program.csView on GitHub
// Find stale Active Directory accounts and see which passwords expire this month.
using System.Globalization;
using ConsoleMonster;
using ConsoleMonster.Dates;
using ConsoleMonster.Layout;
using ConsoleMonster.Presenters;
using ConsoleMonster.Styling;
using ConsoleMonster.Widgets;
using Calendar = ConsoleMonster.Widgets.Calendar;

// Data: fictional accounts and a fixed report date; no domain controller is queried.
var today = new DateTime(2026, 11, 2);
var accounts = new Account[]
{
    new("amy.lee", "Finance", new(2026, 10, 30), new(2026, 8, 20), new DateTime(2026, 11, 18)),
    new("ben.ortiz", "IT", new(2026, 11, 1), new(2026, 8, 6), new DateTime(2026, 11, 4)),
    new("svc-backup", "Service", new(2026, 11, 2), new(2025, 3, 1), null),
    new("cara.diaz", "HR", new(2026, 7, 12), new(2026, 5, 30), new DateTime(2026, 8, 28)),
    new("dan.reyes", "Sales", new(2026, 8, 25), new(2026, 8, 11), new DateTime(2026, 11, 9)),
    new("contractor-07", "External", new(2026, 5, 2), new(2026, 4, 20), new DateTime(2026, 7, 19)),
    new("eve.chen", "Marketing", new(2026, 10, 28), new(2026, 8, 14), new DateTime(2026, 11, 12)),
    new("finn.park", "IT", new(2026, 10, 31), new(2026, 8, 28), new DateTime(2026, 11, 26)),
    new("gia.rossi", "Finance", new(2026, 9, 1), new(2026, 8, 24), new DateTime(2026, 11, 22)),
    new("training-01", "Shared", new(2026, 2, 14), new(2025, 11, 3), null),
    new("hal.novak", "Legal", new(2026, 11, 2), new(2026, 8, 10), new DateTime(2026, 11, 8))
};

// Behaviour: stale means no logon for 60 days or a password older than a year.
var stale = new List<StaleRow>();
foreach (var account in accounts)
{
    int inactive = (today - account.LastLogon).Days;
    int age = (today - account.PasswordSet).Days;
    if (inactive > 60 || age > 365)
    {
        string status = inactive > 90 || age > 365 ? "Fail" : "Warn";
        stale.Add(new StaleRow(account.Name, account.Department,
            account.LastLogon.ToString("yyyy-MM-dd"), inactive, age, status));
    }
}
var expiring = accounts.Where(account => account.PasswordExpires >= today
    && account.PasswordExpires <= today.AddDays(30));
var marks = expiring
    .Select(account => new CalendarMark(account.PasswordExpires!.Value,
        account.PasswordExpires <= today.AddDays(7) ? StatusKind.Warn : StatusKind.Info,
        account.Name + " password"))
    .Append(new CalendarMark(
        new DateTime(2026, 11, 16),
        StatusKind.Fail,
        $"Disable {stale.Count} accounts"))
    .OrderBy(mark => mark.Date)
    .ToArray();

// Layout: the accounts to act on, then the month of expiries beside the dated list.
var table = Table.For(stale)
    .Column(row => row.Account)
    .Column(row => row.Department)
    .Column(row => row.LastLogon)
    .Column(row => row.Inactive)
    .Column(row => row.PasswordAge)
    .Column(row => row.Status, column => column.Presenter = CellPresenters.StatusWord);
table.Expand = true;

var calendar = new Calendar(2026, 11, marks) { FirstDayOfWeek = DayOfWeek.Monday, Today = today };
var upcoming = new StatusList();
foreach (var mark in marks)
    upcoming.Add(mark.Date.ToString("ddd dd MMM", CultureInfo.GetCultureInfo("en-US")), mark.Status,
        mark.Label);

var report = new Rows(
    new Rule("[bold]Account hygiene[/] [muted]· adatum.example.test · as of 2026-11-02[/]"),
    table,
    new Columns(new Panel(calendar, "November"), new Panel(upcoming, "Coming up")))
{
    Gap = 1
};

// Behaviour: print once; confirm each stale account with its manager before disabling it.
Terminal.Write(report);

record Account(string Name, string Department, DateTime LastLogon, DateTime PasswordSet,
    DateTime? PasswordExpires);

record StaleRow(string Account, string Department, string LastLogon, int Inactive, int PasswordAge,
    string Status);
C# fluentProgram.Fluent.csView on GitHub
// Find stale Active Directory accounts and see which passwords expire this month.
using System.Globalization;
using ConsoleMonster;
using ConsoleMonster.Dates;
using ConsoleMonster.Layout;
using ConsoleMonster.Presenters;
using ConsoleMonster.Styling;
using ConsoleMonster.Widgets;

// Data: fictional accounts and a fixed report date; no domain controller is queried.
var today = new DateTime(2026, 11, 2);
var accounts = new Account[]
{
    new("amy.lee", "Finance", new(2026, 10, 30), new(2026, 8, 20), new DateTime(2026, 11, 18)),
    new("ben.ortiz", "IT", new(2026, 11, 1), new(2026, 8, 6), new DateTime(2026, 11, 4)),
    new("svc-backup", "Service", new(2026, 11, 2), new(2025, 3, 1), null),
    new("cara.diaz", "HR", new(2026, 7, 12), new(2026, 5, 30), new DateTime(2026, 8, 28)),
    new("dan.reyes", "Sales", new(2026, 8, 25), new(2026, 8, 11), new DateTime(2026, 11, 9)),
    new("contractor-07", "External", new(2026, 5, 2), new(2026, 4, 20), new DateTime(2026, 7, 19)),
    new("eve.chen", "Marketing", new(2026, 10, 28), new(2026, 8, 14), new DateTime(2026, 11, 12)),
    new("finn.park", "IT", new(2026, 10, 31), new(2026, 8, 28), new DateTime(2026, 11, 26)),
    new("gia.rossi", "Finance", new(2026, 9, 1), new(2026, 8, 24), new DateTime(2026, 11, 22)),
    new("training-01", "Shared", new(2026, 2, 14), new(2025, 11, 3), null),
    new("hal.novak", "Legal", new(2026, 11, 2), new(2026, 8, 10), new DateTime(2026, 11, 8))
};

// Behaviour: stale means no logon for 60 days or a password older than a year.
var stale = new List<StaleRow>();
foreach (var account in accounts)
{
    int inactive = (today - account.LastLogon).Days;
    int age = (today - account.PasswordSet).Days;
    if (inactive > 60 || age > 365)
    {
        string status = inactive > 90 || age > 365 ? "Fail" : "Warn";
        stale.Add(new StaleRow(account.Name, account.Department,
            account.LastLogon.ToString("yyyy-MM-dd"), inactive, age, status));
    }
}

var expiring = accounts.Where(account => account.PasswordExpires >= today
    && account.PasswordExpires <= today.AddDays(30));
var marks = expiring.Select(account => new CalendarMark(account.PasswordExpires!.Value,
    account.PasswordExpires <= today.AddDays(7) ? StatusKind.Warn : StatusKind.Info,
    account.Name + " password"))
    .Append(new CalendarMark(
        new DateTime(2026, 11, 16),
        StatusKind.Fail,
        $"Disable {stale.Count} accounts"))
    .OrderBy(mark => mark.Date)
    .ToArray();

// Layout: the accounts to act on, then the month of expiries beside the dated list.
var table = Ui.Table(stale)
    .Column(row => row.Account)
    .Column(row => row.Department)
    .Column(row => row.LastLogon)
    .Column(row => row.Inactive)
    .Column(row => row.PasswordAge)
    .Column(row => row.Status, column => column.Presenter(CellPresenters.StatusWord))
    .Expand();
var calendar = Ui.Calendar()
    .Year(2026)
    .Month(11)
    .Marks(marks)
    .FirstDayOfWeek(DayOfWeek.Monday)
    .Today(today);
var upcoming = Ui.StatusList();
foreach (var mark in marks)
    upcoming.Add(mark.Date.ToString("ddd dd MMM", CultureInfo.GetCultureInfo("en-US")), mark.Status,
        mark.Label);
var report = Ui.Rows().Gap(1).Content(c3 =>
{
    c3.Rule("[bold]Account hygiene[/] [muted]· adatum.example.test · as of 2026-11-02[/]");
    c3.Add(table);
    c3.Columns(columns => columns.Content(c2 =>
    {
        c2.Panel(p => p.Title("November").Content(c =>
        {
            c.Add(calendar);
        }));
        c2.Panel(p => p.Title("Coming up").Content(c =>
        {
            c.Add(upcoming);
        }));
    }));
});

// Behaviour: print once; confirm each stale account with its manager before disabling it.
report.Show();

record Account(string Name, string Department, DateTime LastLogon, DateTime PasswordSet,
    DateTime? PasswordExpires);

record StaleRow(string Account, string Department, string LastLogon, int Inactive, int PasswordAge,
    string Status);

Run it yourself

Three steps in PowerShell, four in C#. Every command copies with one click.

  1. Install the module

    Once. Windows PowerShell 5.1 or PowerShell 7 on Windows, macOS or Linux.

    Install-Module ConsoleMonster -Scope CurrentUser
  2. Save the program

    Download it, or copy it from above into stale-accounts.ps1.

  3. Run it

    From the folder you saved it in.

    ./stale-accounts.ps1
  1. Create a project

    A plain console app; .NET 8 or later.

    dotnet new console -n StaleAccounts; cd StaleAccounts
  2. Save the project

    The project selects one C# style at a time and restores ConsoleMonster.

  3. Save both programs

    Replace Program.cs and save Program.Fluent.cs beside it.

  4. Run it

    dotnet run
  1. Create a project

    A plain console app; .NET 8 or later.

    dotnet new console -n StaleAccounts; cd StaleAccounts
  2. Save the project

    The project selects one C# style at a time and restores ConsoleMonster.

  3. Save both programs

    Replace Program.cs and save Program.Fluent.cs beside it.

  4. Run it

    dotnet run --property:DemoStyle=Fluent

Or skip the copying. Every demo ships with the module and the project template.

Show-ConsoleDemo stale-accounts
Export-ConsoleDemo stale-accounts -Path ./stale-accounts.ps1
dotnet new install ConsoleMonster.Templates
dotnet new consolemonster --demo stale-accounts -n StaleAccounts
dotnet new install ConsoleMonster.Templates
dotnet new consolemonster --demo stale-accounts --DemoStyle Fluent -n StaleAccounts

Make it yours

Start with these, then read the rest of the file.

  1. Fill the account rows from Get-ADUser with LastLogonDate, PasswordLastSet and msDS-UserPasswordExpiryTimeComputed.

  2. Change the 60-day, 90-day and one-year rules to match your account policy.

  3. Exclude service accounts you manage elsewhere, or give them their own panel.

More reports

All demos