Перейти к содержимому

Как найти GPO, в которой настроен нужный параметр политики, с помощью PowerShell

В крупном домене Active Directory количество групповых политик (GPO) может исчисляться десятками и сотнями. Когда нужно найти, в какой именно политике задан конкретный параметр — например, «Interactive logon: Machine inactivity limit» — просматривать каждый GPO вручную через консоль управления групповыми политиками (GPMC) слишком долго. Гораздо быстрее сделать это скриптом на PowerShell.

Суть подхода

Модуль GroupPolicy предоставляет командлет Get-GPOReport, который для любой политики может сформировать подробный XML-отчёт со всеми настроенными в ней параметрами — от политик безопасности до административных шаблонов. Идея скрипта проста:

  1. Получить список всех GPO в домене (Get-GPO -All).
  2. Для каждой политики сформировать XML-отчёт (Get-GPOReport -ReportType Xml).
  3. Найти в отчёте нужную строку или параметр — по точному совпадению, маске (*, ?) или регулярному выражению.
  4. Вывести название GPO, конкретную настройку и её значение — чтобы не листать отчёт вручную.

Что умеет готовый скрипт

  • Поиск по маске или regex. Можно искать как по простому шаблону (*inactivity*), так и по точному регулярному выражению — например, чтобы учитывать разное количество пробелов в названии параметра.
  • Настройки прямо в начале файла. Не нужно запоминать параметры командной строки — строка поиска, домен и путь для выгрузки результатов задаются переменными в верхней части скрипта, скрипт можно сразу запускать целиком.
  • Вывод конкретного параметра и его значения, а не просто факта «совпадение найдено». Скрипт разбирает XML-структуру отчёта и показывает, например: Настройка: Machine inactivity limit, Параметры: State=Enabled; SettingNumber=900.
  • Прогресс и тайминг по каждой политике. Для каждого GPO выводится время, потраченное на получение и разбор отчёта — это помогает быстро понять, если поиск «зависает» на конкретной политике или на конкретном контроллере домена.
  • Параллельная обработка. Вместо последовательного опроса сотен GPO по одному скрипт может обрабатывать несколько политик одновременно (через ForEach-Object -Parallel в PowerShell 7+ или runspace pool в PowerShell 5.1), что заметно сокращает общее время поиска в больших доменах.
  • Экспорт результатов в CSV — для дальнейшего анализа, отчётности или передачи коллегам.

Типичные сценарии применения

  • Найти все GPO, где настроена конкретная политика безопасности (например, «Machine inactivity limit», «Password must meet complexity requirements», «Account lockout threshold»).
  • Проверить, не задан ли один и тот же параметр в нескольких конфликтующих политиках.
  • Провести аудит перед изменением или удалением GPO — убедиться, что важная настройка не потеряется.
  • Быстро локализовать источник проблемы, когда на компьютерах применяется неожиданное значение параметра, а вручную искать по десяткам политик через GPMC нет времени.

Итог

Такой скрипт превращает рутинную ручную проверку десятков GPO через графический интерфейс в задачу на несколько секунд-минут: вы задаёте, что ищете, запускаете скрипт — и получаете точный список политик с названием параметра и его значением, а не просто «где-то там написано это слово».

# ============================================================
#                  НАСТРОЙКИ СКРИПТА
# ============================================================

$SearchString = "*Interactive logon*"
$UseRegex     = $false
$DomainName   = $env:USERDNSDOMAIN
$OutputPath   = "C:\temp\gpo_search_results.csv"

$UseParallel  = $true
$MaxThreads   = 8

# ============================================================
#                  ДАЛЬШЕ КОД НЕ ТРОГАЕМ
# ============================================================

if ($UseRegex) {
    $pattern = $SearchString
}
else {
    $escaped = [regex]::Escape($SearchString)
    $escaped = $escaped -replace '\\\*', '.*' -replace '\\\?', '.'
    $pattern = $escaped
}

Write-Host "==============================================="
Write-Host "Домен поиска   : $DomainName"
Write-Host "Строка поиска  : $SearchString $(if ($UseRegex) {'(regex)'} else {'(маска)'})"
Write-Host "Regex-паттерн  : $pattern"
Write-Host "PS версия      : $($PSVersionTable.PSVersion)"
Write-Host "Параллельно    : $UseParallel (потоков: $MaxThreads)"
Write-Host "==============================================="
Write-Host ""

Import-Module GroupPolicy -SkipEditionCheck -ErrorAction Stop

Write-Host "Получаем список всех GPO в домене..."
$swTotal = [System.Diagnostics.Stopwatch]::StartNew()
$allGposInDomain = Get-GPO -All -Domain $DomainName
Write-Host "Найдено GPO: $($allGposInDomain.Count). Список получен за $([math]::Round($swTotal.Elapsed.TotalSeconds,1)) сек.`n"

$total = $allGposInDomain.Count
$results = [System.Collections.Concurrent.ConcurrentBag[object]]::new()

Write-Host "Начинаем поиск...`n"

if ($UseParallel -and $PSVersionTable.PSVersion.Major -ge 7) {

    # -------- PowerShell 7+: весь код обработки — прямо внутри -Parallel --------
    $counterSync = [ref]0

    $allGposInDomain | ForEach-Object -Parallel {

        $gpo        = $_
        $pattern    = $using:pattern
        $DomainName = $using:DomainName
        $bag        = $using:results

        # В каждом новом runspace модуль GroupPolicy надо импортировать заново
        Import-Module GroupPolicy -SkipEditionCheck -ErrorAction SilentlyContinue

        $sw = [System.Diagnostics.Stopwatch]::StartNew()
        $out = [PSCustomObject]@{
            GPOName     = $gpo.DisplayName
            GUID        = $gpo.Id
            Elapsed     = 0
            Matched     = $false
            SettingName = $null
            Parameters  = $null
            Error       = $null
        }

        try {
            [xml]$report = Get-GPOReport -Guid $gpo.Id -ReportType Xml -Domain $DomainName -ErrorAction Stop
            $reportRaw = $report.OuterXml

            if ($reportRaw -match $pattern) {
                $out.Matched = $true
                $allLeafNodes = $report.SelectNodes("//*[not(*)]")

                foreach ($node in $allLeafNodes) {
                    if ($node.InnerText -match $pattern) {
                        $parent = $node.ParentNode
                        $nameNode = $parent.SelectSingleNode("*[local-name()='Name']")
                        $settingName = if ($nameNode) { $nameNode.InnerText } else { $node.LocalName }
                        $paramLine = ($parent.ChildNodes | Where-Object { $_.NodeType -eq 'Element' } |
                            ForEach-Object { "$($_.LocalName)=$($_.InnerText)" }) -join "; "

                        $out.SettingName = $settingName
                        $out.Parameters  = $paramLine
                        break
                    }
                }
            }
        }
        catch {
            $out.Error = $_.Exception.Message
        }

        $sw.Stop()
        $out.Elapsed = [math]::Round($sw.Elapsed.TotalSeconds, 1)

        # Живой прогресс прямо из потока
        $status = if ($out.Error) { "ОШИБКА" } elseif ($out.Matched) { "НАЙДЕНО" } else { "нет совпадений" }
        Write-Host "$($gpo.DisplayName) — $status ($($out.Elapsed) сек)"
        if ($out.Matched) {
            Write-Host "   -> Настройка : $($out.SettingName)"
            Write-Host "      Параметры : $($out.Parameters)"
        }
        if ($out.Error) {
            Write-Host "   -> $($out.Error)"
        }

        if ($out.Matched) { $bag.Add($out) }

    } -ThrottleLimit $MaxThreads
}
else {

    # -------- Последовательный режим (PS 5.1 или UseParallel=$false) --------
    $counter = 0
    foreach ($gpo in $allGposInDomain) {
        $counter++
        Write-Host "[$counter/$total] Проверяем: $($gpo.DisplayName) ..." -NoNewline

        $sw = [System.Diagnostics.Stopwatch]::StartNew()
        $matched = $false
        $settingName = $null
        $paramLine = $null
        $errMsg = $null

        try {
            [xml]$report = Get-GPOReport -Guid $gpo.Id -ReportType Xml -Domain $DomainName -ErrorAction Stop
            $reportRaw = $report.OuterXml

            if ($reportRaw -match $pattern) {
                $matched = $true
                $allLeafNodes = $report.SelectNodes("//*[not(*)]")
                foreach ($node in $allLeafNodes) {
                    if ($node.InnerText -match $pattern) {
                        $parent = $node.ParentNode
                        $nameNode = $parent.SelectSingleNode("*[local-name()='Name']")
                        $settingName = if ($nameNode) { $nameNode.InnerText } else { $node.LocalName }
                        $paramLine = ($parent.ChildNodes | Where-Object { $_.NodeType -eq 'Element' } |
                            ForEach-Object { "$($_.LocalName)=$($_.InnerText)" }) -join "; "
                        break
                    }
                }
            }
        }
        catch {
            $errMsg = $_.Exception.Message
        }

        $sw.Stop()
        $elapsed = [math]::Round($sw.Elapsed.TotalSeconds, 1)

        if ($errMsg) {
            Write-Host " ОШИБКА ($elapsed сек)" -ForegroundColor Red
            Write-Host "   -> $errMsg" -ForegroundColor Red
        }
        elseif ($matched) {
            Write-Host " НАЙДЕНО ($elapsed сек)" -ForegroundColor Green
            Write-Host "   -> Настройка : $settingName" -ForegroundColor Yellow
            Write-Host "      Параметры : $paramLine" -ForegroundColor Cyan
            $results.Add([PSCustomObject]@{
                GPOName = $gpo.DisplayName; GUID = $gpo.Id
                SettingName = $settingName; Parameters = $paramLine; Elapsed = $elapsed
            })
        }
        else {
            Write-Host " нет совпадений ($elapsed сек)" -ForegroundColor DarkGray
        }
    }
}

$swTotal.Stop()
$resultsArray = $results.ToArray()

Write-Host "`n==============================================="
Write-Host "Поиск завершён за $([math]::Round($swTotal.Elapsed.TotalSeconds,1)) сек."
Write-Host "Найдено совпадений: $($resultsArray.Count) из $total GPO"
Write-Host "==============================================="

if ($OutputPath -ne "" -and $resultsArray.Count -gt 0) {
    $resultsArray | Select-Object GPOName, GUID, SettingName, Parameters, Elapsed |
        Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Host "Результаты сохранены в: $OutputPath"
}

$resultsArray | Select-Object GPOName, SettingName, Parameters, Elapsed | Format-Table -AutoSize

Similar Posts:

Добавить комментарий

Яндекс.Метрика