Аудит последнего входа в Active Directory: PowerShell, CSV и точные значения с разных контроллеров
За 15 лет работы с корпоративными доменами я перевидал десятки запросов «нам надо узнать, кто из сотрудников когда последний раз заходил в домен». Иногда это безопасность хочет почистить «спящие» учётки, иногда HR — сверить со списком уволенных, иногда кадровый аудит ИБ-конторы. И почти всегда штатный администратор начинает с команды Get-ADUser -Filter * -Properties LastLogon и удивляется, почему данные «какие-то странные». Разбираю, как делать это правильно, какие подводные камни прячутся в атрибутах AD и как написать одну команду, которая выдаст корректный CSV для отчёта руководству.
Почему «простой» аудит даёт неверные данные
Active Directory хранит информацию о последнем входе, но делает это сразу в нескольких атрибутах, и ни один из них не идеален. Разберёмся, что к чему.
- LastLogon — точное время последнего успешного входа, но хранится только на том контроллере, который аутентифицировал пользователя. На других DC значение пустое или старое. Для домена с 4 контроллерами и 500 пользователями вы получите 4 разные картины.
- LastLogonTimeStamp — реплицируемая версия с задержкой 9–14 дней (атрибут
ms-DS-Logon-Time-Sync-Interval). Видна на всех DC, но грубая. Если пользователь зашёл вчера — значение могло обновиться три дня назад. - LastLogonDate — это просто LastLogonTimeStamp, преобразованный модулем ActiveDirectory в человеческий формат DateTime. Удобно для отчётов.
- PwdLastSet — последняя смена пароля. Не аудит входов, но иногда полезно для проверки активности.
Нужен «плюс-минус две недели» — хватает LastLogonDate. Нужно точно «когда последний раз заходил» (например, для расследования инцидента) — придётся опрашивать все контроллеры. Я покажу оба способа.
Быстрый способ: LastLogonDate для всех пользователей
Самый простой сценарий, с которым я часто сталкиваюсь, это когда нужно узнать, кто из активных пользователей не заходил в домен дольше N дней. Это идеально подходит для вашего еженедельного аудита или для согласования с HR. Вы можете сделать это с любого контроллера домена или даже с RSAT-машины.
Import-Module ActiveDirectory
Get-ADUser -Filter {Enabled -eq $true} `
-Properties LastLogonDate, DisplayName, Department, Title |
Select-Object SamAccountName, DisplayName, Department, Title, LastLogonDate |
Sort-Object LastLogonDate |
Export-Csv "C:\Reports\AD-LastLogon-$(Get-Date -Format yyyyMMdd).csv" `
-NoTypeInformation -Encoding UTF8
Через 30 секунд получаете CSV с отсортированным списком от самых старых дат входа к самым свежим. Идеально для отчёта в Excel — пустые LastLogonDate означают «никогда не входил» (свежесозданный аккаунт или сервисная учётка).
Точный способ: опрос всех контроллеров домена
Но что делать, если нужна точность до минуты? Вот, например, когда ИБ требует узнать «во сколько именно последний раз заходил Иванов перед увольнением»? В таких случаях мы берём атрибут LastLogon. Тут есть нюанс: он не реплицируется, а значит, придётся вручную опросить каждый контроллер домена и потом выбрать максимальное значение.
$user = "ivanov"
$dcs = (Get-ADDomainController -Filter *).HostName
$logons = foreach ($dc in $dcs) {
$u = Get-ADUser -Identity $user -Server $dc -Properties LastLogon
[PSCustomObject]@{
DC = $dc
LastLogon = if ($u.LastLogon) { [DateTime]::FromFileTime($u.LastLogon) } else { $null }
}
}
$logons | Sort-Object LastLogon -Descending | Format-Table -AutoSize
$max = ($logons | Where-Object LastLogon | Measure-Object LastLogon -Maximum).Maximum
Write-Host "Last logon (real): $max" -ForegroundColor Green
Для всех пользователей сразу — то же самое в цикле, но на больших доменах это занимает время. На 1000 пользователей × 4 контроллера это 4000 LDAP-запросов, занимает 5–15 минут в зависимости от сети.
Поиск неактивных учётных записей за один запрос
А если ваша задача звучит прямо так: «дайте всех, кто не заходил 90 дней», то, к счастью, для этого есть отдельный командлет, который написан специально под такие запросы.
Search-ADAccount -AccountInactive -TimeSpan 90.00:00:00 -UsersOnly |
Where-Object {$_.Enabled -eq $true} |
Select-Object SamAccountName, Name, LastLogonDate, DistinguishedName |
Export-Csv "C:\Reports\Inactive-Users-90d.csv" -NoTypeInformation -Encoding UTF8
Командлет умный — он смотрит реплицированный LastLogonTimeStamp и работает на любом контроллере. Аналогичные команды есть для компьютеров (-ComputersOnly) и для заблокированных учёток (-LockedOut).
Чек-лист регулярного аудита неактивных учёток
В АйТи Фреш мы у клиентов настраиваем регулярный аудит по такой схеме:
- Раз в неделю — отчёт «не заходили 30 дней». Отправляем HR для сверки с увольнениями.
- Раз в две недели — отчёт «не заходили 60 дней». Согласовываем отключение учёток с непосредственным руководителем.
- Раз в месяц — отключение всех, кто не заходил 90 дней, с переносом в OU «Disabled». В описании учётки фиксируем дату отключения.
- Раз в полгода — удаление учёток, лежащих в Disabled больше 180 дней. Перед удалением — экспорт всех атрибутов и членств в группах в JSON-бэкап.
Скрипт для пункта 3, который мы запускаем по расписанию:
$inactiveDays = 90
$disabledOU = "OU=Disabled Users,DC=corp,DC=example,DC=ru"
$logFile = "C:\Logs\AD-Disable-$(Get-Date -Format yyyyMMdd).log"
Search-ADAccount -AccountInactive -TimeSpan "$inactiveDays.00:00:00" -UsersOnly |
Where-Object {$_.Enabled -eq $true -and $_.DistinguishedName -notlike "*OU=Service*"} |
ForEach-Object {
try {
Disable-ADAccount -Identity $_.SamAccountName
Move-ADObject -Identity $_.DistinguishedName -TargetPath $disabledOU
Set-ADUser -Identity $_.SamAccountName `
-Description "Disabled $(Get-Date -Format 'yyyy-MM-dd') — inactive $inactiveDays+ days"
Add-Content $logFile "OK $($_.SamAccountName) - $($_.LastLogonDate)"
} catch {
Add-Content $logFile "FAIL $($_.SamAccountName) - $_"
}
}
Send-MailMessage -From "ad-audit@itfresh.ru" -To "admin@itfresh.ru" `
-Subject "AD inactive accounts disabled $(Get-Date -Format yyyy-MM-dd)" `
-Body (Get-Content $logFile -Raw) -SmtpServer "mail.itfresh.ru"
Запуск аудита через Task Scheduler с gMSA
Для автоматических задач в Task Scheduler создаю gMSA — managed service account, у которого пароль ротируется автоматически каждые 30 дней. Никогда не используйте Domain Admin для расписаний.
# Создание KDS Root Key (один раз на лес)
Add-KdsRootKey -EffectiveTime ((Get-Date).AddHours(-10))
# Создание gMSA
New-ADServiceAccount -Name "gmsa-adscripts" `
-DNSHostName "gmsa-adscripts.corp.itfresh.ru" `
-PrincipalsAllowedToRetrieveManagedPassword "DC01$","DC02$"
# Установка на сервере, где будут крутиться скрипты
Install-ADServiceAccount -Identity "gmsa-adscripts"Регистрация задачи:
$action = New-ScheduledTaskAction -Execute "pwsh.exe" `
-Argument "-NoProfile -File C:\Scripts\AD-Inactive-Audit.ps1"
$trigger = New-ScheduledTaskTrigger -Daily -At 02:00
$principal = New-ScheduledTaskPrincipal -UserId "CORP\gmsa-adscripts$" -LogonType Password
Register-ScheduledTask -TaskName "AD-Inactive-Audit-Daily" `
-Action $action -Trigger $trigger -Principal $principalАудит компьютеров: забытые в домене ноутбуки
Помимо пользователей, в AD копится мусор из компьютерных учёток. Списанные ноутбуки, переустановленные ПК с новым именем, виртуалки из старых тестов. Они не безобидны — на них могут оставаться сертификаты, права в Wi-Fi, секреты Kerberos. Аудит — той же командой:
Search-ADAccount -AccountInactive -TimeSpan 60.00:00:00 -ComputersOnly |
Where-Object {$_.Enabled -eq $true} |
Select-Object Name, LastLogonDate, OperatingSystem, DistinguishedName |
Sort-Object LastLogonDate |
Export-Csv "C:\Reports\Inactive-Computers.csv" -NoTypeInformation -Encoding UTF8
На любом домене 100+ ПК этот отчёт покажет 5–15 «забытых» машин. Я обычно отключаю их сразу, а удаляю через 30 дней — на случай, если ПК просто долго лежал на полке и его принесли обратно.
Реальный кейс: 22 «спящих» учётки в торговой сети
Я хорошо помню, как в сентябре 2025 года к нам обратилась одна торговая сеть. У них было 14 розничных точек и центральный офис, а домен насчитывал 45 пользователей. Запрос от службы безопасности был очень чётким: «У нас за 4 года скопилось слишком много старых учёток, их нужно срочно почистить. Аудитор по 152-ФЗ обнаружил 2 уволенных сотрудника, у которых до сих пор активен VPN-доступ».
За полтора часа мы выгрузили полную картину:
- Активных, но не заходивших более 90 дней — 18 учёток.
- Активных, но не заходивших более 365 дней — 7 учёток.
- В группах с правами на корпоративные ресурсы из «спящих» — 4 (включая 1 учётку с правом удалённого доступа на сервер 1С).
- Сервисных учёток с просроченным паролем (PwdLastSet старше 730 дней) — 2.
После тщательной сверки с HR-отделом картина, наконец, прояснилась: выяснилось, что 15 учёток принадлежали реальным уволенным сотрудникам, которых почему-то забыли отключить при расчёте. Ещё 3 учётные записи оказались сотрудниками в длительных декретах или на больничных – мы пометили их в описании и, конечно, отключать не стали. В итоге, всего за три рабочих дня мы отключили все 15, аккуратно перенесли их в OU Disabled, отозвали сертификаты PKI и удалили из всех VPN-групп. А уже через месяц, когда не поступило ни одной жалобы, мы их окончательно удалили. Стоимость этих работ составила 9 800 руб. Кстати, с тех пор у этого клиента успешно работает наш скрипт, который каждую неделю отправляет свежий отчёт прямо на почту директора по безопасности.
Распространённые ошибки в скриптах аудита
За все те годы, что я собираю подобные отчёты, мне приходилось сталкиваться с довольно типовыми ошибками. И, поверьте, они очень сильно искажают данные:
- Запрос только LastLogon без преобразования. Атрибут возвращает Int64 в формате FileTime. Без
[DateTime]::FromFileTime()вы видите большое число и не понимаете, что это. - Игнорирование сервисных учёток. svc-account для 1С может «не входить» месяцами с точки зрения интерактивного входа, но активно работать через Kerberos. Фильтруйте их через атрибут
servicePrincipalNameили отдельную OU. - Запрос на одном контроллере и неправильный вывод. Если вы запросили LastLogon только на DC01, а пользователь логинился через DC02, увидите пустое значение. Используйте LastLogonTimeStamp для общей картины или опрашивайте все DC для точности.
- Удаление без бэкапа. Прежде чем
Remove-ADUser, всегда экспортируйте все атрибуты и членства в группы в JSON. Бывают случаи, когда «уволенный» оказывается ушедшим в декрет. - Игнорирование Disabled accounts. Учётка может быть отключена, но всё ещё членом групп с доступом к ресурсам. Проверяйте и Enabled=true, и членства в чувствительных группах отдельно.
Логирование и уведомления в Telegram
Любой production-скрипт аудита обязан писать журнал и присылать уведомление, когда что-то пошло не так. Иначе скрипт молча падает три недели подряд, и админ узнаёт об этом, когда уже накопилась куча проблем. Минимальный шаблон-обёртка:
$LogPath = "C:\Logs\AD\$(Get-Date -Format yyyy-MM-dd)_$($MyInvocation.MyCommand.Name).log"
Start-Transcript -Path $LogPath -Append
try {
Write-Host "[$(Get-Date)] Старт"
# основной код скрипта аудита
Write-Host "[$(Get-Date)] Готово"
} catch {
$err = "$($_.Exception.Message)`n$($_.ScriptStackTrace)"
Write-Host "[$(Get-Date)] ОШИБКА: $err" -ForegroundColor Red
$tgToken = "BOT_TOKEN"
$tgChat = "-100xxxxxxxxxx"
$message = "AD-script $($MyInvocation.MyCommand.Name) FAILED on $env:COMPUTERNAME`n$err"
Invoke-RestMethod -Uri "https://api.telegram.org/bot$tgToken/sendMessage" -Method Post `
-Body @{chat_id=$tgChat; text=$message; parse_mode="Markdown"}
} finally {
Stop-Transcript
}Логи храню 90 дней, по утрам через скрипт-агрегатор сканирую ключевое слово ERROR — если за ночь что-то упало, в Telegram прилетает сводка.
Bonus: однострочник для скоростного аудита
Представьте ситуацию: кто-то из руководства внезапно попросил: «Срочно дай список тех, кто не заходил больше месяца!» и вам нужен максимально быстрый ответ. Что делать?
Get-ADUser -Filter {Enabled -eq $true} -Properties LastLogonDate |
Where-Object {$_.LastLogonDate -lt (Get-Date).AddDays(-30)} |
Select-Object SamAccountName, Name, LastLogonDate |
Format-Table -AutoSize
За 5 секунд получаете табличку прямо в консоли. Дальше — Ctrl+A, Ctrl+C, вставить в письмо.
FAQ — частые вопросы по аудиту последнего входа
- Чем отличаются LastLogon и LastLogonTimeStamp?
- LastLogon — точное время входа, но хранится только на том контроллере, к которому пользователь обращался, и не реплицируется. LastLogonTimeStamp реплицируется между контроллерами, но обновляется с задержкой 9–14 дней (по умолчанию).
- Как получить точную дату последнего входа?
- Опросить все контроллеры домена и взять максимальное значение LastLogon с каждого. Это даёт точность до минуты, но требует обращения ко всем DC, что на крупных доменах занимает время.
- Как массово найти неактивных пользователей?
- Используйте Search-ADAccount -AccountInactive -TimeSpan 90.00:00:00 -UsersOnly. Команда вернёт всех пользователей, не входивших в систему 90 дней. Дальше — экспорт в CSV, согласование с HR, отключение учёток.
- Что делать с неактивными учётками?
- Стандартная процедура: при отсутствии активности 90 дней — отключение через Disable-ADAccount, перенос в OU Disabled. После 180 дней без обращений HR — удаление через Remove-ADUser с предварительным резервным копированием атрибутов.
- Как часто запускать аудит неактивных учёток?
- Минимум раз в месяц, лучше — раз в две недели через запланированную задачу с отправкой отчёта на почту администратора. На динамичных доменах с частой текучкой — еженедельно.
Источники
- Microsoft Learn: атрибут Last-Logon-Timestamp — реплицируемый атрибут и его задержка обновления.
- Microsoft Learn: атрибут Last-Logon — нереплицируемый lastLogon, который надо опрашивать на каждом DC.
- Microsoft Learn: Search-ADAccount — поиск неактивных учётных записей параметром -AccountInactive.
Похожие статьи
- Pending reboot на Windows-серверах: полный аудит через PowerShell
- FGPP в Active Directory: разные политики паролей для разных ролей
- PowerShell для Active Directory: 12 рабочих скриптов на 2026 год
- Аудит перезагрузок Windows Server: находим виновника за 10 минут
