Загрузка данных


#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