Загрузка данных
#region ===== Конфигурация =====
$DomainName = "mydomain.organization.ru"
$OUUserServices = "OU=ServiceAccounts,DC=mydomain,DC=organization,DC=ru"
$OUServers = "OU=Servers,DC=mydomain,DC=organization,DC=ru"
$ResultUserAccounts = "C:\Reports\UserAccounts.csv"
$LogRetentionDays = 30
#endregion
#region ===== Функции =====
function Get-TargetAccounts {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$OUUserServices
)
try {
Get-ADUser -Server $DomainName -SearchBase $OUUserServices -SearchScope Subtree `
-Filter { Enabled -eq $true } `
-Properties DisplayName, SamAccountName, UserPrincipalName |
Select-Object @{n='DisplayName'; e={ if ($_.DisplayName) { $_.DisplayName } else { $_.SamAccountName } }},
SamAccountName, UserPrincipalName
} catch {
Write-Warning "Не удалось получить учётные записи из $OUUserServices : $_"
}
}
function Get-TargetServers {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$OUServers
)
try {
Get-ADComputer -Server $DomainName -SearchBase $OUServers -SearchScope Subtree `
-Filter { Enabled -eq $true } -Properties DNSHostName |
Select-Object -ExpandProperty DNSHostName -ErrorAction SilentlyContinue
} catch {
Write-Warning "Не удалось получить серверы из $OUServers : $_"
}
}
function Resolve-AccountFromIdentity {
[CmdletBinding()]
param(
[string]$Identity,
[Parameter(Mandatory)][hashtable]$Lookup
)
if ([string]::IsNullOrWhiteSpace($Identity)) { return $null }
$id = $Identity.Trim()
# Универсальная обработка: DOMAIN\user, user, user@domain
$short = ($id -split '\\')[-1]
$short = ($short -split '@')[0].ToLower()
if ($Lookup.ContainsKey($short)) { return $Lookup[$short] }
if ($Lookup.ContainsKey($id.ToLower())) { return $Lookup[$id.ToLower()] }
return $null
}
function Get-ServerServices {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$ComputerName)
try {
Get-CimInstance -ClassName Win32_Service -ComputerName $ComputerName -ErrorAction Stop |
Select-Object Name, DisplayName, StartName
} catch {
Write-Verbose "Службы недоступны на $ComputerName : $_"
return $null
}
}
function Get-ServerScheduledTasks {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$ComputerName)
try {
$raw = schtasks /query /s $ComputerName /fo CSV /v 2>$null
if (-not $raw) { return $null }
$raw | ConvertFrom-Csv |
Where-Object { $_."Run As User" -and $_."Run As User" -ne 'N/A' } |
Select-Object TaskName, @{n='StartName'; e={ $_."Run As User" }}
} catch {
Write-Verbose "Планировщик недоступен на $ComputerName : $_"
return $null
}
}
function Get-ServerIISAppPools {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$ComputerName)
try {
Invoke-Command -ComputerName $ComputerName -ErrorAction Stop -ScriptBlock {
Import-Module WebAdministration -ErrorAction Stop
Get-ChildItem IIS:\AppPools | ForEach-Object {
$identity = $_.processModel.userName
if ($identity) {
[PSCustomObject]@{ Name = $_.Name; StartName = $identity }
}
}
}
} catch {
Write-Verbose "IIS недоступен на $ComputerName : $_"
return $null
}
}
function Get-DCLogonEvents {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$AccountSam,
[Parameter(Mandatory)][string]$DomainName,
[int]$Days = 30
)
$results = New-Object System.Collections.Generic.List[object]
try {
$dcs = (Get-ADDomainController -Server $DomainName -Filter *).Name
} catch {
Write-Warning "Не удалось получить список DC: $_"
return $results
}
$startTime = (Get-Date).AddDays(-$Days)
foreach ($dc in $dcs) {
try {
Get-WinEvent -ComputerName $dc -FilterHashtable @{
LogName = 'Security'
ID = 4624
StartTime = $startTime
} -ErrorAction Stop |
Where-Object {
$_.Properties[5].Value -eq $AccountSam -and
($_.Properties[8].Value -in @(3,4,5,8,9,10))
} | ForEach-Object {
$src = $_.Properties[18].Value
if ($src -and $src -ne '-') {
$src = $src -replace '^\\\\', ''
$results.Add([PSCustomObject]@{
Server = $src
WhereUsed = "Журнал входа DC (4624, logon type $($_.Properties[8].Value))"
})
}
}
} catch {
Write-Verbose "Не удалось прочитать журнал на $dc : $_"
}
}
return $results
}
function Merge-ScanResults {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$CsvPath,
[Parameter(Mandatory)][AllowEmptyCollection()][System.Collections.Generic.List[object]]$NewResults
)
$now = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
# Существующий CSV → словарь по ключу "Логин|Сервер|Где используется"
$merged = @{}
if (Test-Path $CsvPath) {
$existing = Import-Csv -Path $CsvPath -Encoding UTF8
foreach ($row in $existing) {
$k = "$($row.'Логин')|$($row.'Имя сервера')|$($row.'Где используется')"
$merged[$k] = $row
}
}
# Обновление / добавление
foreach ($newRow in $NewResults) {
$k = "$($newRow.'Логин')|$($newRow.'Имя сервера')|$($newRow.'Где используется')"
if ($merged.ContainsKey($k)) {
$merged[$k].'Дата получения информации' = $now
if ($newRow.'Имя учетной записи') {
$merged[$k].'Имя учетной записи' = $newRow.'Имя учетной записи'
}
} else {
$merged[$k] = [PSCustomObject]@{
'Имя учетной записи' = $newRow.'Имя учетной записи'
'Логин' = $newRow.'Логин'
'Имя сервера' = $newRow.'Имя сервера'
'Где используется' = $newRow.'Где используется'
'Дата получения информации' = $now
}
}
}
# Записи, которые есть в старом CSV, но не найдены в этом скане — остаются как есть
# (дата НЕ обновляется).
$dir = Split-Path $CsvPath -Parent
if ($dir -and -not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
$merged.Values |
Sort-Object 'Логин', 'Имя сервера', 'Где используется' |
Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8
}
function Invoke-ServiceAccountScan {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$OUUserServices,
[Parameter(Mandatory)][string]$OUServers,
[Parameter(Mandatory)][string]$ResultPath,
[int]$LogDays = 30
)
$accounts = Get-TargetAccounts -DomainName $DomainName -OUUserServices $OUUserServices
if (-not $accounts) { Write-Warning "Нет учётных записей для проверки."; return }
$servers = Get-TargetServers -DomainName $DomainName -OUServers $OUServers
if (-not $servers) { Write-Warning "Нет серверов для проверки."; return }
Write-Host "Учётных записей: $($accounts.Count); серверов: $($servers.Count)"
# Хэш-таблица быстрого поиска: SamAccountName/UPN → объект учётной записи
$lookup = @{}
foreach ($acc in $accounts) {
if ($acc.SamAccountName) { $lookup[$acc.SamAccountName.ToLower()] = $acc }
if ($acc.UserPrincipalName) { $lookup[$acc.UserPrincipalName.ToLower()] = $acc }
}
$results = New-Object System.Collections.Generic.List[object]
# ---- 1. Сканирование серверов (службы, задачи, IIS) ----
$i = 0
foreach ($server in $servers) {
$i++
Write-Progress -Activity "Сканирование серверов" `
-Status "$server ($i/$($servers.Count))" `
-PercentComplete (($i / $servers.Count) * 100)
foreach ($svc in (Get-ServerServices -ComputerName $server)) {
$acc = Resolve-AccountFromIdentity -Identity $svc.StartName -Lookup $lookup
if ($acc) {
$results.Add([PSCustomObject]@{
'Имя учетной записи' = $acc.DisplayName
'Логин' = $acc.SamAccountName
'Имя сервера' = $server
'Где используется' = "Служба: $($svc.DisplayName) ($($svc.Name))"
})
}
}
foreach ($t in (Get-ServerScheduledTasks -ComputerName $server)) {
$acc = Resolve-AccountFromIdentity -Identity $t.StartName -Lookup $lookup
if ($acc) {
$results.Add([PSCustomObject]@{
'Имя учетной записи' = $acc.DisplayName
'Логин' = $acc.SamAccountName
'Имя сервера' = $server
'Где используется' = "Планировщик: $($t.TaskName)"
})
}
}
foreach ($p in (Get-ServerIISAppPools -ComputerName $server)) {
$acc = Resolve-AccountFromIdentity -Identity $p.StartName -Lookup $lookup
if ($acc) {
$results.Add([PSCustomObject]@{
'Имя учетной записи' = $acc.DisplayName
'Логин' = $acc.SamAccountName
'Имя сервера' = $server
'Где используется' = "IIS пул: $($p.Name)"
})
}
}
}
Write-Progress -Activity "Сканирование серверов" -Completed
# ---- 2. Анализ журналов безопасности DC ----
$j = 0
foreach ($acc in $accounts) {
$j++
Write-Progress -Activity "Анализ журналов DC" `
-Status "$($acc.SamAccountName) ($j/$($accounts.Count))" `
-PercentComplete (($j / $accounts.Count) * 100)
foreach ($ev in (Get-DCLogonEvents -AccountSam $acc.SamAccountName -DomainName $DomainName -Days $LogDays)) {
$results.Add([PSCustomObject]@{
'Имя учетной записи' = $acc.DisplayName
'Логин' = $acc.SamAccountName
'Имя сервера' = $ev.Server
'Где используется' = $ev.WhereUsed
})
}
}
Write-Progress -Activity "Анализ журналов DC" -Completed
# ---- 3. Слияние и сохранение ----
Merge-ScanResults -CsvPath $ResultPath -NewResults $results
Write-Host "Готово. Результат: $ResultPath"
}
#endregion
#region ===== Запуск =====
Invoke-ServiceAccountScan `
-DomainName $DomainName `
-OUUserServices $OUUserServices `
-OUServers $OUServers `
-ResultPath $ResultUserAccounts `
-LogDays $LogRetentionDays
#endregion