Часто мы ищем готовые решения, качаем софт, просим доступы — а инструмент уже лежит под рукой. У меня была рутинная задача: проверять учетки пользователей в AD. Когда менялся пароль, есть ли блокировка, не истек ли срок действия. Каждый раз — открыть ADUC, найти учетку, прокликать вкладки. Минута-две на запрос, десять запросов в день — и вот уже часы уходят в никуда.

В какой-то момент я подумал: но ведь есть PowerShell. И написал скрипт, который помогает за секунду получить состояние учетки, дату смены пароля, блокировки, контакты, подразделение. Можно сразу снять временную блокировку. Вдруг и вам пригодится.

Что получится в итоге

Скрипт, который по логину выводит 30+ атрибутов пользователя из AD. Если у пользователя временная блокировка — скрипт предложит ее снять. Один инструмент вместо десятка кликов по консолям.

Смотрим, что есть в AD

Прежде чем писать скрипт, нужно понять, какие данные доступны. Открываем PowerShell ISE (он есть в Windows из коробки) и вытаскиваем все атрибуты своей учетки:

Get-ADUser -Identity Ваш_логин -Properties * | Select-Object -Property * | ForEach-Object { ($_ -split ',') } | Out-File -FilePath "c:\$login.txt"

В файле окажется масса информации: имя, дата создания учетки, последняя смена пароля, блокировки и десятки других полей. Чтобы понять, как работать с каждым атрибутом, нужны их свойства:

Get-ADUser -Identity Ваш_логин -Properties * | Get-Member | ForEach-Object { ($_ -split ',') } | Out-File -FilePath "c:\Properties.txt"

Например, строка System.DateTime AccountLockoutTime {get;set;} говорит, что AccountLockoutTime — это дата и время. Сам атрибут хранит момент временной блокировки учетной записи (той, которую вы запросили). Зная типы данных, можно правильно их обрабатывать и форматировать.

Каркас 

Начнем с основы. PowerShell, в отличие от того же Bash, поддерживает обработку исключений — грех этим не воспользоваться:

try {
    $login=Read-Host "Enter login"
    if (-not $login) {
        Write-Host "Пользователь не найден!"
    } else { GetAdUserInfo $login }
} catch { Write-Warning "Что-то пошло не так: $($_.Exception.Message)" }

Функцию GetAdUserInfo я назвал по правилам хорошего тона — с глагола Get. Теперь напишем ее.

Простая версия 

Для начала — базовый вариант, чтобы понять принцип:

function GetAdUserInfo {
    Param (
        [string]$login
    )

    try {    
    $user=Get-AdUser $login -Properties * 
    $user | Select-Object @{Name="Login";expression={$_.SamAccountName}},`
            @{Name="Отображаемое имя (ФИО)";expression={$_.DisplayName -replace "`n"," "}},`
            @{Name="Фамилия";expression={$_.Surname -replace "`n"," "}},`
            @{Name="Временная блокировка";expression={if ($_.AccountLockoutTime) { `
            Write-Output "Временная блокировка с "; $locktime=$_.AccountLockoutTime; Write-Output $locktime.ToString()  } else {`
            Write-Output "Отсутствует" }}}#, `
        #   @{ Другие значения }, ` бэктик нужен для переноса на новую строку, лучше чем писать в горизонт за широту монитора :) 
        #   @{ Другие значения } 
    } catch { Write-Warning "Что-то пошло не так в функции GetAdUserInfo: $($_.Exception.Message)" }
}

Здесь мы получаем все атрибуты пользователя и через хеш-таблицу формируем читаемый вывод. Со значениями можно работать сразу, например высчитать, сколько дней пользователю до смены пароля или когда он его сменил.

Если команда Get-ADUser не работает, добавьте в начало скрипта: Import-Module ActiveDirectory.

Полная версия

Универсальный скрипт для всех написать не получится — атрибуты и их свойства в ваших доменах могут отличаться. Так что открывайте ISE и построчно делайте для себя рабочий скрипт.

Пишите скрипт под конкретное рутинное действие с учетом частоты использования — где-то проще и быстрее открыть консоль для разовых действий. Здесь как в известном произведении: лучше день потерять, но потом за час долететь. Написав за вечер, использовать будете годами, экономя время и здоровье.

Вот что мне удалось вытянуть на своем рабочем месте:

Import-Module ActiveDirectory

function GetAdUserInfo {
    Param (
        [Parameter(Mandatory=$true)]
        [string]$login,
        $global:locktime=$null
    )

    try {    
    [int64]$nullDate=9223372036854775807
    $dftPwdPolicy=Get-ADDefaultDomainPasswordPolicy -ErrorAction Stop
    $dftPwdChangePeriod=$dftPwdPolicy.MaxPasswordAge.Days 
    $today=Get-Date

    $u=Get-AdUser $login -Properties *
    $u | select @{Name="Login";expression={$_.SamAccountName}},`
                @{Name="Отображаемое имя (ФИО)";expression={$_.DisplayName -replace "`n"," "}},`
                @{Name="Фамилия";expression={$_.Surname -replace "`n"," "}},`
                @{Name="Имя";expression={$_.GivenName  -replace "`n"," "}},`
                @{Name="Отчество";expression={$_.OtherName  -replace "`n"," "}},`
                @{Name="Состояние";expression={$expired=$false; if ($nullDate -ne [int64]$_.accountexpires -and 0 -ne [int64]$_.accountexpires){$expDate=[DateTime]::FromFileTime($_.accountexpires); if ($today -gt $expDate){$expired=$true}};if (!$_.Enabled){"отключена"} else {if ($expired) {"истек срок действия"} else {if ($_.PasswordLastSet -eq $null){"начальный пароль не менялся"} else { if ([datetime]($_.PasswordLastSet) -ge $today ){"просрочена смена пароля"}else {"OK"}}}}}},`
                @{Name="Пароль изменен (дней до смены пароля)";expression={`
                        if (!$_.PasswordNeverExpires) {`
                            if ($_.PasswordLastSet -ne $null){`
                                $pwdLastSet=[datetime]($_.PasswordLastSet);`

                                $maxPasswordAge = $dftPwdChangePeriod;`
                                
                                if ($_."msDS-ResultantPSO" -ne $null)`
                                {`
                                    $PasswordPol = $_ | Get-ADUserResultantPasswordPolicy -Server $Server; `    
                                    $maxPasswordAge = ($PasswordPol).MaxPasswordAge.Days`
                                }`

                                $diff=(New-TimeSpan -Start $today -End ($pwdLastSet).AddDays($maxPasswordAge)).Days; `
                                [string]$s=($pwdLastSet).ToString('dd.MM.yyyy HH:mm:ss'); `
                                $s+ " ($diff)"`
                            } else { "Не менялся" + " (0)" }`
                        } else {"Неистекаемый"}`
                    }`
                },`
                @{Name="Последний вход";expression={if ($null -ne $_.lastlogontimestamp ) {[DateTime]::FromFileTime($_.lastlogontimestamp).ToString('dd.MM.yyyy HH:mm:ss')}else {""}}},`
                @{Name="Номер телефона";expression={$_.telephoneNumber  -replace "`n"," "}},`
                @{Name="Идентификатор почты";expression={$_.extensionName[0]}},`
                @{Name="Внешний адрес";expression={$_.mail}},`
                @{Name="Идентификатор БОСС";expression={$_.EmployeeID}},`
                @{Name="Табельный номер";expression={$_.employeeNumber}},`
                @{Name="Дата рождения";expression={$_.ExtensionAttribute4}},`
                @{Name="Должность";expression={$_.title  -replace "`n"," "}},`
                @{Name="Подразделение";expression={$_.division  -replace "`n"," "}},`
                @{Name="Департамент";expression={$_.department  -replace "`n"," "}},`
                @{Name="Организация";expression={$_.company  -replace "`n"," "}},`
                @{Name="Срок действия";expression={if ($nullDate -ne [int64]$_.accountexpires  -and 0 -ne [int64]$_.accountexpires){[DateTime]::FromFileTime($_.accountexpires).ToString('dd.MM.yyyy HH:mm:ss')}else {""}}}, `
                @{Name="OrgUnit";expression={ if ($_.distinguishedName -match "^CN=.*?,(?<OU>(CN|OU)=.*)"){$Matches.OU} else {""} }}, `
                @{Name="SPN";expression={ if ($_.servicePrincipalName -ne $null){"задан"} else {""} }}, `
                @{Name="Дата создания";expression={([datetime]$_.whenCreated).ToString('dd.MM.yyyy HH:mm:ss')}}, `
                @{Name="Дата изменения";expression={([datetime]$_.whenChanged).ToString('dd.MM.yyyy HH:mm:ss')}}, `
                @{Name="Дата попытки входа с неверным паролем";expression={if ($nullDate -ne [int64]$_.badPasswordTime  -and 0 -ne [int64]$_.badPasswordTime ) {[DateTime]::FromFileTime($_.badPasswordTime).ToString('dd.MM.yyyy HH:mm:ss')}}}, `
                @{Name="Количество ошибок входа";expression={$_.badPwdCount}}, `
                @{Name="Количество входов";expression={$_.logonCount}}, `
                @{Name="Город";expression={$_.city}}, `
                @{Name="Почтовый адрес";expression={$_.postalAddress}}, `
                @{Name="Комната";expression={$_.roomNumber}}, `
                @{Name="IP телефон";expression={$_.ipPhone}}, `
                @{Name="Политика паролей (имя)";expression={if ($_."msDS-ResultantPSO" -eq $null){"Основная"}else{($_ | Get-ADUserResultantPasswordPolicy).name}}}, `
                @{Name="Временная блокировка";expression={if ($_.AccountLockoutTime) {Write-Output "блокировка с"; $global:locktime=$_.AccountLockoutTime; Write-Output $locktime.ToString() } else { Write-Output "Отсутствует" }}}
    } catch { Write-Warning "Что-то пошло не так в функции GetAdUserInfo: $($_.Exception.Message)" }
    
}


try {
    $login=Read-Host "Enter login"
    Write-Output "Результаты поиска атрибутов в АД у пользователя" 
    if (-not $login) {
        Write-Host "Пользователь не найден!"
    } else {
        GetAdUserInfo $login
        if ($global:locktime -ne $null) {
            $request = Read-Host "`nВведите y для снятия временной блокировки или любой символ для НЕТ"
                if ($request -eq 'y') {  Unlock-ADAccount -Identity $login
                                         Write-Output "Временная блокировка снята"
                } else { Write-Warning "`nУ пользователя блокировка осталась НЕ снятой $($_.Exception.Message)"}
        } 
     }
} catch { Write-Warning "Error: $($_.Exception.Message)" }
       

Read-Host "`nВведите любой символ или пробел и нажмите Enter для выхода"

Важно: у вас атрибуты могут называться по-другому или вообще отсутствовать!

Бонус

Раз уж мы делаем набор инструментов для AD, вот еще один полезный скрипт. Когда нужно найти группу, но помнишь только часть названия:

Import-Module ActiveDirectory
    
try {
    $group = Read-Host "Введите группу для поиска"
    $SearchGroups = Get-ADGroup -Filter "Name -like '*$group*'"
    if (-not $SearchGroups) {
            Write-Host "not search '$group' in AD"
    } else {
            $SearchGroups | Sort-Object | ForEach-Object { ($_ -split ',')[0] -replace '^CN=', '' } | ForEach-Object { Write-Host $_ }
        }

} catch {Write-Warning "Error: $($_.Exception.Message)" }

Результат приходит по конвейеру: массив → сортировка → разделили по запятой, заменив пустым символом ненужный префикс 'CN=' → построчно вывели.

Теперь то же самое с алиасами — заменим ForEach-Object на %и вывод в консоль на echo:

Import-Module ActiveDirectory
    
try {
    $group = Read-Host "Введите группу для поиска"
    $SearchGroups = Get-ADGroup -Filter "Name -like '*$group*'"
    if (-not $SearchGroups) {
            echo "not search '$group' in AD"
    } else {
            $SearchGroups | sort | % { ($_ -split ',')[0] -replace '^CN=', '' } | % { echo $_ }
        }

} catch {Write-Warning "Error: $($_.Exception.Message)" }

Работает так же, но стало лаконичнее. Посмотреть все доступные алиасы можно командой Get-Alias | Sort-Object Name.

Итого

Часто все необходимые инструменты уже под рукой. PowerShell — это мощная штука, которую многие игнорируют. А зря. Он есть на любой Windows-машине, умеет работать с AD, файлами, сетью, реестром и не требует ни установки, ни согласований. Просто открываете ISE и делаете, что нужно.

Если хотите углубиться в тему, вот полезные статьи на Хабре:

Комментарии (13)


  1. Arhammon
    02.12.2025 07:28

    PowerShell, в отличие от того же Bash, поддерживает обработку исключений

    Не то что бы знаток линуксов, но помниться оно там есть пусть и не называется try...


    1. pavelmvl
      02.12.2025 07:28

      Выглядит примерно так:

      {
      cmd1;
      cmd2;
      cmd3;
      } || echo "Что-то пошло не так"


      1. mavir
        02.12.2025 07:28

        Если глобально, то в начале ставим флаг завершаться с ошибкой при ошибке выполнения какой-то команды

        set -e

        Затем с помощью trap ловим падения

        catch_error() {
          echo "Я упаль"
        }
        trap catch_error ERR


        1. xaht88 Автор
          02.12.2025 07:28

          Да, везде проявить можно смекалку! :) Правда современные тенденции выражены в простоте и лаконичности для языка, PoSh ввёл исключения и классы не сразу, но они появились. Лично мне проще сразу для линукса писать на Paython ему скрипты, тут как бы удобство. Да и будущее уже наступило, если недавно придумали Kotlin приблизив написание кода к знанию английского, то теперь везде внедряют генераторы кода в помощь разработчику, в котором можно писать на родном бытовом языке и получать машинный код в ответ.


    1. Busla
      02.12.2025 07:28

      да по большому счёту в скриптах из статьи и не нужны исключения - они только переформулируют стандартные ошибки, что только путает и мешает поиску проблем


  1. phili_can
    02.12.2025 07:28

    Powershell поддерживает работу с формами, например $form=new-object windows.forms.form . Давно уже все скрипты где что-то надо вводить/выводить делаю через окна и кнопки. Можно даже в exe скомпилировать, при желании.


    1. Busla
      02.12.2025 07:28

      ...и потом это никак не переиспользовать, не вызвать из других скриптов, не передать по конвейеру и т.п.

      Если скрипт получает данные через параметры, то простые формы генерятся автоматически коммандлетом Show-Command.


  1. kerberos464
    02.12.2025 07:28

    Powershell - это сила, но у вас очень странные юзкейсы.

    У меня была рутинная задача: проверять учетки пользователей в AD. Когда менялся пароль, есть ли блокировка, не истек ли срок действия.

    Зачем знать, когда менялся пароль у учётки? Если надо, чтобы была регулярная смена паролей, для этого есть политика.

    Зачем знать, есть ли блокировка? Если есть автоистечение блокировки, ручные действия не требуются. Если нет - юзер сам придёт.

    Зачем знать, не истёк ли срок действия, чтобы потом «что»? Тут даже вменяемого варианта придумать не могу.


    1. A11mas
      02.12.2025 07:28

      del


    1. Crazy_Father
      02.12.2025 07:28

      Поддерживаю, сам столкнулся с подобной задачей, решил похожим образом, но проще и быстрее, скрипт на вход принимает логин пользователя, причем берет его из буфера обмена, так быстрее, после меняет пароль и кладет в буфер обмена готовую текстовку с новым паролем и необходимыми данными. Которую мы просто вставляем туда куда нужно письмо/тикет система..... Это быстро. При этом можно сделать, чтобы на вход скрипта принималось полное ФИО, Так тоже работает. Зависит от ваших ИТ процессов.


    1. dz75
      02.12.2025 07:28

      Частый кейс: пользователь звонит в панике: "у меня никуда не входит!". Соответственно посмотрел и дальше варианты:
      - истек срок действия пароля (кто ж читает все эти письма-уведомления про истекающий срок действия пароля): в пятнадцатый раз направляем пользователю инструкцию по смене пароля.
      - закончился срок действия УЗ: тут уже к ПМ вопрос, почему он проигнорировал уведомление, что у его разраба УЗ скоро истекает.
      - УЗ заблокирована из за попыток авторизации с неверным паролем: если таймаут закончился, то ничего не делаем, иначе сбросить блокировку или начать разбираться, откуда авторизации идут.
      Да, и у меня тоже есть такой самописный скрипт, который отображает состояние УЗ и позволяет сделать сброс блокировки.


  1. crawlingroof
    02.12.2025 07:28

    Да ладно вам, задушнили, человек старался, написал, поделился. Кому-то зайдет, хотябы в качестве примера. Все лучше чем очередной ИИшный выплеск. Циферки закончились, завтра +поставлю


    1. saintmonday
      02.12.2025 07:28

      Согласен. У нас полностью виндовая структура и далёких 7 лет назад я по таким вот статьям собирал по крупицам информацию. Для новичков супер :)