Один админ на 200 человек: 20 PowerShell-скриптов для страховой компании

Задача клиента: один администратор, 200 сотрудников и рутина

К нам обратилась страховая компания из Екатеринбурга с типичной для среднего бизнеса проблемой: единственный IT-администратор тонул в рутинных задачах. 200 сотрудников, 8 серверов, Active Directory, файловые шары, почтовый сервер — и на всё это один человек, который каждое утро вручную проверял диски, просматривал журналы событий, разблокировал учётки и генерировал отчёты для руководства.

По нашей оценке, на рутинные проверки и обслуживание администратор тратил до 3 часов в день — 15 часов в неделю, которые можно было направить на развитие инфраструктуры. Мы предложили комплексное решение: набор из 20 PowerShell-скриптов, покрывающих все ежедневные задачи, с автоматическим запуском по расписанию и email-уведомлениями.

Каждый скрипт мы протестировали в инфраструктуре клиента, снабдили подробными комментариями и настроили для автономной работы. Все скрипты совместимы с PowerShell 5.1 (Windows Server 2019 клиента) и PowerShell 7.

Подготовка: настройка рабочего окружения

Первым делом наши инженеры подготовили среду выполнения скриптов на серверах клиента.

Что мы настроили перед внедрением

На основном сервере управления мы выполнили подготовку:

# Проверка версии PowerShell
$PSVersionTable.PSVersion

# Настройка политики выполнения скриптов
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

# Установка необходимых модулей
Install-Module -Name ActiveDirectory -Force
Install-Module -Name ImportExcel -Force  # Для экспорта в Excel без Office
Install-Module -Name PSWriteHTML -Force  # Для HTML-отчётов

# Создание структуры каталогов для скриптов
New-Item -ItemType Directory -Path C:\Scripts\Reports -Force
New-Item -ItemType Directory -Path C:\Scripts\Logs -Force
New-Item -ItemType Directory -Path C:\Scripts\Backup -Force

Все скрипты мы разместили в C:\Scripts и настроили их запуск через Task Scheduler.

Принципы, которых мы придерживались

При разработке скриптов для клиента мы следовали правилам, которые делают скрипты пригодными для продуктивной среды:

  • Обработка ошибок: везде используем try/catch и $ErrorActionPreference = 'Stop'
  • Логирование: каждый скрипт записывает действия в лог-файл с датой и временем
  • Параметризация: все переменные (серверы, пути, email) вынесены в начало скрипта
  • Идемпотентность: повторный запуск скрипта не создаёт проблем
  • Комментарии: каждый скрипт содержит описание, автора и дату — для IT-специалиста клиента

Блок 1: скрипты мониторинга дисков и ресурсов

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

Скрипт 1: Мониторинг свободного места на дисках

Этот скрипт мы настроили на запуск каждый час — он проверяет все 8 серверов клиента и отправляет алерт при заполнении диска:

# Скрипт 1: Мониторинг дискового пространства
# Разработан инженерами АйТи Фреш для страховой компании

param(
    [int]$ThresholdPercent = 15,
    [string]$SmtpServer = 'mail.company.ru',
    [string]$AdminEmail = 'admin@company.ru'
)

$servers = @('SRV-DC01', 'SRV-FILE01', 'SRV-SQL01', 'SRV-APP01')
$alerts = @()

foreach ($server in $servers) {
    try {
        $disks = Get-WmiObject -Class Win32_LogicalDisk `
            -ComputerName $server -Filter "DriveType=3" -ErrorAction Stop

        foreach ($disk in $disks) {
            $freePercent = [math]::Round(($disk.FreeSpace / $disk.Size) * 100, 1)
            $freeGB = [math]::Round($disk.FreeSpace / 1GB, 1)
            $totalGB = [math]::Round($disk.Size / 1GB, 1)

            if ($freePercent -lt $ThresholdPercent) {
                $alerts += [PSCustomObject]@{
                    Server     = $server
                    Drive      = $disk.DeviceID
                    TotalGB    = $totalGB
                    FreeGB     = $freeGB
                    FreePercent = $freePercent
                    Status     = if ($freePercent -lt 5) { 'CRITICAL' } else { 'WARNING' }
                }
            }
        }
    } catch {
        $alerts += [PSCustomObject]@{
            Server = $server; Drive = 'N/A'; TotalGB = 0
            FreeGB = 0; FreePercent = 0; Status = "UNREACHABLE: $_"
        }
    }
}

if ($alerts.Count -gt 0) {
    $html = $alerts | ConvertTo-Html -Title 'Disk Space Alert' `
        -PreContent "<h2>Disk Space Alert — $(Get-Date)</h2>" | Out-String
    Send-MailMessage -From 'monitor@company.ru' -To $AdminEmail `
        -Subject "[DISK ALERT] $($alerts.Count) warnings" `
        -Body $html -BodyAsHtml -SmtpServer $SmtpServer -Encoding UTF8
}

Скрипт 2: Мониторинг CPU и RAM в реальном времени

Скрипт, который мы настроили для записи истории нагрузки серверов — позволяет находить закономерности и планировать апгрейды:

# Скрипт 2: Мониторинг CPU и RAM на серверах клиента

$servers = @('SRV-DC01', 'SRV-FILE01', 'SRV-SQL01')
$logFile = "C:\Scripts\Logs\ResourceUsage_$(Get-Date -Format 'yyyyMMdd').csv"
$results = @()

foreach ($server in $servers) {
    try {
        $cpu = Get-WmiObject -Class Win32_Processor -ComputerName $server |
            Measure-Object -Property LoadPercentage -Average |
            Select-Object -ExpandProperty Average

        $os = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $server
        $totalRAM = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1)
        $freeRAM = [math]::Round($os.FreePhysicalMemory / 1MB, 1)
        $usedRAMPercent = [math]::Round((1 - $os.FreePhysicalMemory / $os.TotalVisibleMemorySize) * 100, 1)

        $results += [PSCustomObject]@{
            Timestamp   = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
            Server      = $server
            CPU_Percent = $cpu
            RAM_TotalGB = $totalRAM
            RAM_FreeGB  = $freeRAM
            RAM_UsedPct = $usedRAMPercent
        }
    } catch {
        Write-Warning "Не удалось подключиться к $server : $_"
    }
}

$results | Format-Table -AutoSize
$results | Export-Csv -Path $logFile -Append -NoTypeInformation -Encoding UTF8

Скрипт 3: Топ процессов по потреблению памяти

Администратор клиента регулярно жаловался на «медленные серверы». Этот скрипт мы написали для быстрой диагностики утечек памяти:

# Скрипт 3: Топ-10 процессов по потреблению RAM

param(
    [string]$ComputerName = $env:COMPUTERNAME,
    [int]$TopN = 10
)

$processes = Get-Process -ComputerName $ComputerName |
    Sort-Object -Property WorkingSet64 -Descending |
    Select-Object -First $TopN -Property @(
        'Name',
        'Id',
        @{Name='RAM_MB'; Expression={[math]::Round($_.WorkingSet64 / 1MB, 0)}},
        @{Name='CPU_Sec'; Expression={[math]::Round($_.CPU, 1)}},
        @{Name='Handles'; Expression={$_.HandleCount}},
        @{Name='Threads'; Expression={$_.Threads.Count}},
        @{Name='StartTime'; Expression={$_.StartTime}}
    )

$processes | Format-Table -AutoSize

$html = $processes | ConvertTo-Html -Title "Top $TopN Processes — $ComputerName" `
    -PreContent "<h2>Top $TopN процессов по RAM на $ComputerName</h2><p>$(Get-Date)</p>"
$html | Out-File -FilePath "C:\Scripts\Reports\TopProcesses_$ComputerName.html"

Скрипт 4: Мониторинг сетевых подключений

После инцидента с подозрительными соединениями с одного из ПК мы добавили скрипт анализа сетевой активности:

# Скрипт 4: Анализ сетевых подключений

Get-NetTCPConnection -State Established, TimeWait, CloseWait |
    Group-Object -Property State |
    Select-Object Count, Name |
    Sort-Object Count -Descending |
    Format-Table -AutoSize

Write-Host "`n--- Топ-10 удалённых адресов по количеству соединений ---" -ForegroundColor Cyan
Get-NetTCPConnection -State Established |
    Group-Object -Property RemoteAddress |
    Sort-Object Count -Descending |
    Select-Object -First 10 Count, Name |
    Format-Table -AutoSize

Write-Host "`n--- Прослушиваемые порты ---" -ForegroundColor Cyan
Get-NetTCPConnection -State Listen |
    Select-Object LocalPort, @{
        Name='Process'; Expression={
            (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName
        }
    } | Sort-Object LocalPort | Format-Table -AutoSize

Блок 2: скрипты Active Directory

Active Directory в страховой компании — центральный компонент инфраструктуры. 200 пользователей, частая ротация кадров, строгие требования регулятора к безопасности. Мы автоматизировали отчёты и аудит AD, что сэкономило клиенту десятки часов ежемесячно.

Скрипт 5: Отчёт по неактивным пользователям AD

В страховой компании текучка кадров выше среднего — забытые учётки представляли риск безопасности. Мы внедрили еженедельную проверку:

# Скрипт 5: Поиск неактивных пользователей AD

param(
    [int]$InactiveDays = 90,
    [string]$ExportPath = 'C:\Scripts\Reports\InactiveUsers.csv'
)

Import-Module ActiveDirectory

$threshold = (Get-Date).AddDays(-$InactiveDays)

$inactiveUsers = Get-ADUser -Filter {
    LastLogonDate -lt $threshold -and Enabled -eq $true
} -Properties LastLogonDate, Created, Department, Title, Manager,
    PasswordLastSet, PasswordNeverExpires -SearchBase 'DC=company,DC=ru' |
    Select-Object @(
        'SamAccountName',
        'Name',
        'Department',
        'Title',
        @{Name='Manager'; Expression={
            if ($_.Manager) { (Get-ADUser $_.Manager).Name } else { 'Не назначен' }
        }},
        'LastLogonDate',
        'PasswordLastSet',
        'PasswordNeverExpires',
        'Created',
        @{Name='InactiveDays'; Expression={
            if ($_.LastLogonDate) {
                (New-TimeSpan -Start $_.LastLogonDate -End (Get-Date)).Days
            } else { 'Никогда не входил' }
        }}
    ) | Sort-Object LastLogonDate

Write-Host "Найдено неактивных пользователей: $($inactiveUsers.Count)" -ForegroundColor Yellow
$inactiveUsers | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8

Скрипт 6: Аудит членства в привилегированных группах

Требование регулятора — контроль доступа привилегированных учёток. Мы настроили ежедневный аудит:

# Скрипт 6: Аудит привилегированных групп AD

$privilegedGroups = @(
    'Domain Admins',
    'Enterprise Admins',
    'Schema Admins',
    'Account Operators',
    'Server Operators',
    'Backup Operators',
    'Administrators'
)

$report = @()

foreach ($group in $privilegedGroups) {
    try {
        $members = Get-ADGroupMember -Identity $group -Recursive -ErrorAction Stop
        foreach ($member in $members) {
            $user = Get-ADUser -Identity $member.SamAccountName `
                -Properties LastLogonDate, Enabled, PasswordLastSet
            $report += [PSCustomObject]@{
                Group            = $group
                UserName         = $user.SamAccountName
                DisplayName      = $user.Name
                Enabled          = $user.Enabled
                LastLogon        = $user.LastLogonDate
                PasswordLastSet  = $user.PasswordLastSet
            }
        }
    } catch {
        Write-Warning "Ошибка при обработке группы $group : $_"
    }
}

$report | Sort-Object Group, UserName | Format-Table -AutoSize
$report | Export-Csv 'C:\Scripts\Reports\PrivilegedGroups.csv' `
    -NoTypeInformation -Encoding UTF8

Write-Host "`n--- Сводка ---" -ForegroundColor Green
$report | Group-Object Group | Select-Object Count, Name | Format-Table

Скрипт 7: Массовое создание пользователей из CSV

При открытии нового филиала клиенту потребовалось создать 30 учёток за день. Мы подготовили скрипт массового создания с транслитерацией:

# Скрипт 7: Массовое создание пользователей AD из CSV

param(
    [string]$CsvPath = 'C:\Scripts\Data\NewUsers.csv',
    [string]$OUPath = 'OU=Users,OU=Company,DC=company,DC=ru',
    [string]$Domain = 'company.ru'
)

Import-Module ActiveDirectory
$users = Import-Csv -Path $CsvPath -Encoding UTF8
$created = 0; $errors = 0

foreach ($user in $users) {
    $login = ($user.FirstName.Substring(0,1) + $user.LastName).ToLower()
    $translitMap = @{
        'а'='a';'б'='b';'в'='v';'г'='g';'д'='d';'е'='e';'ё'='yo';
        'ж'='zh';'з'='z';'и'='i';'й'='y';'к'='k';'л'='l';'м'='m';
        'н'='n';'о'='o';'п'='p';'р'='r';'с'='s';'т'='t';'у'='u';
        'ф'='f';'х'='kh';'ц'='ts';'ч'='ch';'ш'='sh';'щ'='shch';
        'ы'='y';'э'='e';'ю'='yu';'я'='ya';'ъ'='';'ь'=''
    }
    $loginTranslit = ''
    foreach ($char in $login.ToCharArray()) {
        $loginTranslit += if ($translitMap.ContainsKey([string]$char)) {
            $translitMap[[string]$char]
        } else { $char }
    }

    try {
        New-ADUser -SamAccountName $loginTranslit `
            -UserPrincipalName "$loginTranslit@$Domain" `
            -Name "$($user.LastName) $($user.FirstName)" `
            -GivenName $user.FirstName `
            -Surname $user.LastName `
            -Department $user.Department `
            -Title $user.Title `
            -Path $OUPath `
            -AccountPassword (ConvertTo-SecureString $user.Password -AsPlainText -Force) `
            -Enabled $true `
            -ChangePasswordAtLogon $true `
            -ErrorAction Stop

        Write-Host "[OK] Создан: $loginTranslit" -ForegroundColor Green
        $created++
    } catch {
        Write-Warning "[FAIL] $loginTranslit : $_"
        $errors++
    }
}

Write-Host "`nИтого: создано $created, ошибок $errors" -ForegroundColor Cyan

Скрипты 8-9: Поиск заблокированных учёток и контроль паролей

В страховой компании блокировки учёток происходили ежедневно — сотрудники забывали пароли после отпуска. Мы автоматизировали поиск и уведомления:

# Скрипт 8: Поиск заблокированных учётных записей и источника блокировки

Import-Module ActiveDirectory

$locked = Search-ADAccount -LockedOut | Get-ADUser -Properties LockedOut,
    LastBadPasswordAttempt, BadLogonCount, LockoutTime, Department

$locked | Select-Object Name, SamAccountName, Department,
    BadLogonCount, LastBadPasswordAttempt,
    @{Name='LockoutTime'; Expression={
        [DateTime]::FromFileTime($_.LockoutTime)
    }} | Format-Table -AutoSize

$pdc = (Get-ADDomainController -Discover -Service PrimaryDC).HostName[0]
foreach ($user in $locked) {
    Write-Host "`nИсточник блокировки для $($user.SamAccountName):" -ForegroundColor Yellow
    Get-WinEvent -ComputerName $pdc -FilterHashtable @{
        LogName   = 'Security'
        Id        = 4740
    } -MaxEvents 5 | Where-Object {
        $_.Properties[0].Value -eq $user.SamAccountName
    } | Select-Object TimeCreated,
        @{Name='Source'; Expression={$_.Properties[1].Value}} |
        Format-Table -AutoSize
}
# Скрипт 9: Отчёт об истекающих паролях
# Предупреждаем сотрудников заранее — меньше звонков в техподдержку

param([int]$DaysAhead = 14)

$maxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.Days
$warningDate = (Get-Date).AddDays($DaysAhead)

$expiringUsers = Get-ADUser -Filter {
    Enabled -eq $true -and PasswordNeverExpires -eq $false
} -Properties PasswordLastSet, Mail, Department |
    Where-Object {
        $_.PasswordLastSet -and
        ($_.PasswordLastSet.AddDays($maxPasswordAge)) -lt $warningDate -and
        ($_.PasswordLastSet.AddDays($maxPasswordAge)) -gt (Get-Date)
    } | Select-Object Name, SamAccountName, Mail, Department,
        @{Name='PasswordExpires'; Expression={
            $_.PasswordLastSet.AddDays($maxPasswordAge)
        }},
        @{Name='DaysLeft'; Expression={
            ($_.PasswordLastSet.AddDays($maxPasswordAge) - (Get-Date)).Days
        }} | Sort-Object DaysLeft

$expiringUsers | Format-Table -AutoSize
Write-Host "Пользователей с истекающим паролем (< $DaysAhead дней): $($expiringUsers.Count)"

Блок 3: скрипты управления службами и журналами

Управление службами Windows и анализ журналов событий — ежедневные задачи, на которые администратор клиента тратил по часу каждое утро. Мы автоматизировали всё.

Скрипт 10: Автоматический перезапуск критичных служб

Этот скрипт мы настроили на запуск каждые 15 минут — он контролирует ключевые службы и перезапускает их при остановке:

# Скрипт 10: Мониторинг и автоперезапуск критичных служб

$criticalServices = @(
    @{Name='Spooler';       DisplayName='Служба печати'},
    @{Name='MSSQLSERVER';   DisplayName='SQL Server'},
    @{Name='W3SVC';         DisplayName='IIS Web Server'},
    @{Name='WinRM';         DisplayName='Windows Remote Management'},
    @{Name='DNS';           DisplayName='DNS Server'}
)

$logFile = "C:\Scripts\Logs\ServiceRestart_$(Get-Date -Format 'yyyyMMdd').log"

function Write-Log($message) {
    $entry = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | $message"
    Add-Content -Path $logFile -Value $entry
    Write-Host $entry
}

foreach ($svc in $criticalServices) {
    $service = Get-Service -Name $svc.Name -ErrorAction SilentlyContinue
    if (-not $service) {
        Write-Log "[SKIP] $($svc.DisplayName) — служба не найдена"
        continue
    }

    if ($service.Status -ne 'Running') {
        Write-Log "[WARN] $($svc.DisplayName) не запущена (статус: $($service.Status))"

        try {
            Start-Service -Name $svc.Name -ErrorAction Stop
            Start-Sleep -Seconds 5
            $service.Refresh()

            if ($service.Status -eq 'Running') {
                Write-Log "[OK] $($svc.DisplayName) успешно перезапущена"
            } else {
                Write-Log "[FAIL] $($svc.DisplayName) не удалось запустить"
            }
        } catch {
            Write-Log "[ERROR] $($svc.DisplayName): $_"
        }
    } else {
        Write-Log "[OK] $($svc.DisplayName) работает нормально"
    }
}

Скрипт 11: Анализ журналов событий Windows

Вместо ручного просмотра Event Viewer мы настроили автоматический сбор критичных событий:

# Скрипт 11: Анализ критичных событий за последние 24 часа

param(
    [int]$HoursBack = 24,
    [string[]]$LogNames = @('System', 'Application'),
    [string]$ExportPath = 'C:\Scripts\Reports'
)

$startTime = (Get-Date).AddHours(-$HoursBack)
$allEvents = @()

foreach ($logName in $LogNames) {
    Write-Host "Анализ журнала: $logName" -ForegroundColor Cyan

    $events = Get-WinEvent -FilterHashtable @{
        LogName   = $logName
        Level     = 1,2,3
        StartTime = $startTime
    } -ErrorAction SilentlyContinue

    if ($events) {
        $allEvents += $events | Select-Object @(
            'TimeCreated',
            @{Name='Log'; Expression={$logName}},
            @{Name='Level'; Expression={
                switch ($_.Level) {
                    1 {'CRITICAL'}
                    2 {'ERROR'}
                    3 {'WARNING'}
                }
            }},
            'Id',
            'ProviderName',
            'Message'
        )
    }
}

Write-Host "`n--- Топ-10 источников ошибок ---" -ForegroundColor Yellow
$allEvents | Group-Object ProviderName |
    Sort-Object Count -Descending |
    Select-Object -First 10 Count, Name |
    Format-Table -AutoSize

$reportFile = "$ExportPath\EventLog_$(Get-Date -Format 'yyyyMMdd_HHmm').csv"
$allEvents | Export-Csv -Path $reportFile -NoTypeInformation -Encoding UTF8
Write-Host "`nВсего событий: $($allEvents.Count). Отчёт: $reportFile"

Скрипты 12-13: Аудит запланированных задач и автозагрузки

После обнаружения несанкционированной задачи в планировщике на одном из ПК мы добавили скрипты аудита:

# Скрипт 12: Аудит запланированных задач на сервере

$tasks = Get-ScheduledTask | Where-Object {
    $_.State -ne 'Disabled' -and $_.TaskPath -notlike '\Microsoft\*'
} | ForEach-Object {
    $info = $_ | Get-ScheduledTaskInfo -ErrorAction SilentlyContinue
    [PSCustomObject]@{
        TaskName       = $_.TaskName
        TaskPath       = $_.TaskPath
        State          = $_.State
        RunAs          = $_.Principal.UserId
        LastRunTime    = $info.LastRunTime
        LastResult     = $info.LastTaskResult
        NextRunTime    = $info.NextRunTime
        Actions        = ($_.Actions | ForEach-Object {
            "$($_.Execute) $($_.Arguments)"
        }) -join '; '
    }
}

$tasks | Sort-Object TaskPath | Format-Table -AutoSize -Wrap

$failed = $tasks | Where-Object { $_.LastResult -ne 0 -and $_.LastResult -ne $null }
if ($failed) {
    Write-Host "`n[!] Задачи с ошибками:" -ForegroundColor Red
    $failed | Format-Table TaskName, LastRunTime, LastResult -AutoSize
}
# Скрипт 13: Аудит программ в автозагрузке

$regPaths = @(
    'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run',
    'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce',
    'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run',
    'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run'
)

$autostart = @()
foreach ($path in $regPaths) {
    if (Test-Path $path) {
        $items = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
        $items.PSObject.Properties | Where-Object {
            $_.Name -notin @('PSPath','PSParentPath','PSChildName','PSProvider','PSDrive')
        } | ForEach-Object {
            $autostart += [PSCustomObject]@{
                RegistryPath = $path
                Name         = $_.Name
                Command      = $_.Value
            }
        }
    }
}

$autostart | Format-Table -AutoSize -Wrap
Write-Host "Всего записей автозагрузки: $($autostart.Count)"

Блок 4: скрипты резервного копирования

Резервное копирование в страховой компании — требование регулятора. Мы автоматизировали бэкапы файлов, сетевого оборудования и очистку старых данных.

Скрипт 14: Инкрементальный бэкап с ротацией

Для файлового сервера клиента мы настроили инкрементальные бэкапы с автоматической ротацией:

# Скрипт 14: Инкрементальный бэкап каталогов с ротацией

param(
    [string]$Source = 'D:\SharedData',
    [string]$Destination = 'E:\Backups\SharedData',
    [int]$RetainDays = 30
)

$timestamp = Get-Date -Format 'yyyyMMdd_HHmm'
$backupDir = Join-Path $Destination "Backup_$timestamp"
$logFile = Join-Path $Destination "backup_log.txt"

$lastBackup = Get-ChildItem -Path $Destination -Directory |
    Sort-Object CreationTime -Descending | Select-Object -First 1
$sinceDate = if ($lastBackup) { $lastBackup.CreationTime } else { [DateTime]::MinValue }

Write-Host "Бэкап: $Source -> $backupDir"
Write-Host "Файлы изменённые после: $sinceDate"

$files = Get-ChildItem -Path $Source -Recurse -File |
    Where-Object { $_.LastWriteTime -gt $sinceDate }

$copied = 0
foreach ($file in $files) {
    $relativePath = $file.FullName.Substring($Source.Length)
    $destFile = Join-Path $backupDir $relativePath
    $destDir = Split-Path $destFile -Parent

    if (-not (Test-Path $destDir)) {
        New-Item -ItemType Directory -Path $destDir -Force | Out-Null
    }

    Copy-Item -Path $file.FullName -Destination $destFile -Force
    $copied++
}

$summary = "$(Get-Date) | Скопировано файлов: $copied | Размер: $(
    [math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1)
) МБ"
Add-Content -Path $logFile -Value $summary
Write-Host $summary -ForegroundColor Green

$old = Get-ChildItem -Path $Destination -Directory |
    Where-Object { $_.CreationTime -lt (Get-Date).AddDays(-$RetainDays) }
if ($old) {
    $old | Remove-Item -Recurse -Force
    Write-Host "Удалено старых бэкапов: $($old.Count)" -ForegroundColor Yellow
}

Скрипт 15: Бэкап конфигурации сетевого оборудования

У клиента были Cisco-коммутаторы и FortiGate-файрвол. Мы настроили автоматический бэкап конфигурации по SSH:

# Скрипт 15: Бэкап конфигурации сетевого оборудования по SSH

param(
    [string]$BackupDir = 'C:\Scripts\Backup\NetworkConfig'
)

Import-Module Posh-SSH

$devices = @(
    @{Name='SW-CORE-01'; IP='10.0.0.1';  Type='Cisco'; Cmd='show running-config'},
    @{Name='SW-ACCESS-01'; IP='10.0.0.2'; Type='Cisco'; Cmd='show running-config'},
    @{Name='FW-01'; IP='10.0.0.254';      Type='FortiGate'; Cmd='show full-configuration'}
)

$credential = Get-Credential -Message 'Учётные данные для сетевого оборудования'
$date = Get-Date -Format 'yyyyMMdd'

foreach ($device in $devices) {
    try {
        $session = New-SSHSession -ComputerName $device.IP `
            -Credential $credential -AcceptKey -ConnectionTimeout 10

        $stream = New-SSHShellStream -SessionId $session.SessionId
        Start-Sleep -Seconds 2

        if ($device.Type -eq 'Cisco') {
            $stream.WriteLine('terminal length 0')
            Start-Sleep -Seconds 1
        }

        $stream.WriteLine($device.Cmd)
        Start-Sleep -Seconds 5
        $config = $stream.Read()

        $filePath = "$BackupDir\$($device.Name)_$date.txt"
        $config | Out-File -FilePath $filePath -Encoding UTF8

        Write-Host "[OK] $($device.Name) — сохранено в $filePath" -ForegroundColor Green
        Remove-SSHSession -SessionId $session.SessionId | Out-Null
    } catch {
        Write-Warning "[FAIL] $($device.Name): $_"
    }
}

Скрипт 16: Очистка временных файлов и старых логов

На файловом сервере клиента скопилось 40 ГБ временных файлов. Мы внедрили автоматическую очистку:

# Скрипт 16: Очистка временных файлов и устаревших данных

param([int]$RetainDays = 30)

$pathsToClean = @(
    @{Path='C:\Windows\Temp'; Pattern='*'},
    @{Path='C:\Windows\Logs'; Pattern='*.log'},
    @{Path='C:\inetpub\logs\LogFiles'; Pattern='*.log'},
    @{Path="$env:TEMP"; Pattern='*'},
    @{Path='C:\Windows\SoftwareDistribution\Download'; Pattern='*'}
)

$totalFreed = 0

foreach ($item in $pathsToClean) {
    if (-not (Test-Path $item.Path)) { continue }

    $oldFiles = Get-ChildItem -Path $item.Path -Filter $item.Pattern `
        -Recurse -File -ErrorAction SilentlyContinue |
        Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$RetainDays) }

    if ($oldFiles) {
        $size = ($oldFiles | Measure-Object Length -Sum).Sum
        $totalFreed += $size

        $oldFiles | Remove-Item -Force -ErrorAction SilentlyContinue
        Write-Host "$($item.Path): удалено $($oldFiles.Count) файлов " +
            "($([math]::Round($size / 1MB, 1)) МБ)" -ForegroundColor Green
    }
}

Clear-RecycleBin -Force -ErrorAction SilentlyContinue

Write-Host "`nВсего освобождено: $([math]::Round($totalFreed / 1MB, 1)) МБ" -ForegroundColor Cyan

Блок 5: скрипты безопасности и сертификатов

Страховая компания обрабатывает персональные данные клиентов, поэтому мониторинг безопасности — не опция, а обязательное требование. Мы внедрили автоматический контроль сертификатов и аудит попыток доступа.

Скрипт 17: Проверка сроков SSL-сертификатов

После инцидента, когда истёкший сертификат на портале клиентов привёл к 4 часам простоя, мы настроили упреждающий мониторинг:

# Скрипт 17: Мониторинг сроков действия SSL/TLS сертификатов

param(
    [int]$WarningDays = 30,
    [string]$SmtpServer = 'mail.company.ru',
    [string]$AdminEmail = 'admin@company.ru'
)

Write-Host "--- Локальные сертификаты ---" -ForegroundColor Cyan
$localCerts = Get-ChildItem -Path Cert:\LocalMachine\My -Recurse |
    Where-Object { $_.NotAfter -lt (Get-Date).AddDays($WarningDays) -and
                   $_.NotAfter -gt (Get-Date) } |
    Select-Object Subject, Thumbprint, NotAfter,
        @{Name='DaysLeft'; Expression={($_.NotAfter - (Get-Date)).Days}}

$localCerts | Format-Table -AutoSize

$webHosts = @('portal.company.ru', 'mail.company.ru', 'vpn.company.ru')
$remoteCerts = @()

foreach ($host_ in $webHosts) {
    try {
        $tcpClient = New-Object System.Net.Sockets.TcpClient($host_, 443)
        $sslStream = New-Object System.Net.Security.SslStream(
            $tcpClient.GetStream(), $false,
            { param($s,$c,$ch,$e) $true }
        )
        $sslStream.AuthenticateAsClient($host_)
        $cert = $sslStream.RemoteCertificate
        $x509 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($cert)

        $remoteCerts += [PSCustomObject]@{
            Host     = $host_
            Subject  = $x509.Subject
            Issuer   = $x509.Issuer
            NotAfter = $x509.NotAfter
            DaysLeft = ($x509.NotAfter - (Get-Date)).Days
        }

        $sslStream.Close()
        $tcpClient.Close()
    } catch {
        Write-Warning "Не удалось проверить $host_ : $_"
    }
}

Write-Host "`n--- Удалённые сертификаты ---" -ForegroundColor Cyan
$remoteCerts | Format-Table -AutoSize

$expiring = @($localCerts) + @($remoteCerts | Where-Object { $_.DaysLeft -lt $WarningDays })
if ($expiring.Count -gt 0) {
    $body = $expiring | ConvertTo-Html -Title 'Certificate Expiry Alert' | Out-String
    Send-MailMessage -From 'certs@company.ru' -To $AdminEmail `
        -Subject "[CERT ALERT] $($expiring.Count) certificates expiring" `
        -Body $body -BodyAsHtml -SmtpServer $SmtpServer
}

Скрипт 18: Аудит безопасности — попытки несанкционированного доступа

Для выполнения требований регулятора мы настроили ежедневный аудит неудачных попыток входа:

# Скрипт 18: Аудит неудачных попыток входа

param([int]$HoursBack = 24)

$startTime = (Get-Date).AddHours(-$HoursBack)

$failedLogins = Get-WinEvent -FilterHashtable @{
    LogName   = 'Security'
    Id        = 4625
    StartTime = $startTime
} -ErrorAction SilentlyContinue | ForEach-Object {
    [PSCustomObject]@{
        Time       = $_.TimeCreated
        Account    = $_.Properties[5].Value
        Domain     = $_.Properties[6].Value
        SourceIP   = $_.Properties[19].Value
        LogonType  = switch ($_.Properties[10].Value) {
            2  {'Interactive (console)'}
            3  {'Network (SMB/mapped)'}
            7  {'Unlock'}
            8  {'NetworkCleartext'}
            10 {'RemoteDesktop (RDP)'}
            default {$_.Properties[10].Value}
        }
        FailReason = switch ($_.Properties[7].Value) {
            '0xC000006D' {'Bad username or password'}
            '0xC000006A' {'Wrong password'}
            '0xC0000234' {'Account locked'}
            '0xC0000072' {'Account disabled'}
            '0xC000015B' {'Logon type not allowed'}
            default {$_.Properties[7].Value}
        }
    }
}

Write-Host "Неудачных попыток входа за $HoursBack ч: $($failedLogins.Count)" `
    -ForegroundColor Yellow

Write-Host "`n--- Топ IP-адресов с неудачными попытками ---" -ForegroundColor Red
$failedLogins | Group-Object SourceIP |
    Sort-Object Count -Descending |
    Select-Object -First 10 Count, Name |
    Format-Table -AutoSize

Write-Host "--- Топ атакуемых учётных записей ---" -ForegroundColor Red
$failedLogins | Group-Object Account |
    Sort-Object Count -Descending |
    Select-Object -First 10 Count, Name |
    Format-Table -AutoSize

$failedLogins | Export-Csv 'C:\Scripts\Reports\FailedLogins.csv' `
    -NoTypeInformation -Encoding UTF8

Блок 6: сетевая диагностика и ежедневный отчёт

Завершающие скрипты — для сетевой диагностики и автоматического ежедневного отчёта руководству. Именно ежедневный отчёт стал визитной карточкой проекта — директор по ИТ каждое утро получает красивый HTML-дайджест о состоянии инфраструктуры.

Скрипт 19: Комплексная сетевая диагностика

Администратор клиента использует этот скрипт для быстрой диагностики при жалобах пользователей на «не работает интернет»:

# Скрипт 19: Комплексная сетевая диагностика хоста

param(
    [string]$TargetHost = '8.8.8.8',
    [string]$DNSName = 'google.com'
)

Write-Host "=== Сетевая диагностика ==="  -ForegroundColor Cyan
Write-Host "Цель: $TargetHost / $DNSName" -ForegroundColor Cyan
Write-Host "Время: $(Get-Date)" -ForegroundColor Cyan

Write-Host "`n--- 1. Сетевые адаптеры ---" -ForegroundColor Yellow
Get-NetIPConfiguration | Where-Object { $_.IPv4DefaultGateway } |
    Select-Object InterfaceAlias, IPv4Address, IPv4DefaultGateway,
        @{Name='DNS'; Expression={$_.DNSServer.ServerAddresses -join ', '}} |
    Format-Table -AutoSize

Write-Host "--- 2. Ping $TargetHost ---" -ForegroundColor Yellow
$ping = Test-Connection -TargetName $TargetHost -Count 5 -ErrorAction SilentlyContinue
if ($ping) {
    $ping | Select-Object Address, Latency, Status | Format-Table -AutoSize
    $avg = ($ping | Measure-Object Latency -Average).Average
    Write-Host "Средняя задержка: $([math]::Round($avg, 1)) мс"
} else {
    Write-Host "Хост недоступен!" -ForegroundColor Red
}

Write-Host "`n--- 3. Traceroute до $TargetHost ---" -ForegroundColor Yellow
Test-Connection -TargetName $TargetHost -Traceroute -ErrorAction SilentlyContinue |
    Select-Object Hop, Address, Latency | Format-Table -AutoSize

Write-Host "--- 4. DNS-разрешение $DNSName ---" -ForegroundColor Yellow
Resolve-DnsName -Name $DNSName -Type A -ErrorAction SilentlyContinue | Format-Table -AutoSize
Resolve-DnsName -Name $DNSName -Type MX -ErrorAction SilentlyContinue | Format-Table -AutoSize

Write-Host "--- 5. Проверка портов $TargetHost ---" -ForegroundColor Yellow
$ports = @(80, 443, 22, 3389, 445, 53)
foreach ($port in $ports) {
    $result = Test-NetConnection -ComputerName $TargetHost -Port $port `
        -WarningAction SilentlyContinue
    $status = if ($result.TcpTestSucceeded) {'OPEN'} else {'CLOSED'}
    $color = if ($result.TcpTestSucceeded) {'Green'} else {'Red'}
    Write-Host "  Порт ${port}: $status" -ForegroundColor $color
}

Скрипт 20: Ежедневный сводный HTML-отчёт руководству

Гордость проекта — красивый HTML-отчёт, который отправляется каждое утро в 7:00 директору по ИТ и генеральному директору:

# Скрипт 20: Генерация и отправка сводного HTML-отчёта
# Разработан инженерами АйТи Фреш

param(
    [string]$SmtpServer = 'mail.company.ru',
    [string]$AdminEmail = 'admin@company.ru',
    [string]$ReportPath = 'C:\Scripts\Reports'
)

$serverName = $env:COMPUTERNAME
$reportDate = Get-Date -Format 'dd.MM.yyyy HH:mm'

$uptime = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
$uptimeSpan = New-TimeSpan -Start $uptime -End (Get-Date)

$disks = Get-WmiObject Win32_LogicalDisk -Filter 'DriveType=3' |
    Select-Object DeviceID,
        @{Name='SizeGB'; Expression={[math]::Round($_.Size/1GB,1)}},
        @{Name='FreeGB'; Expression={[math]::Round($_.FreeSpace/1GB,1)}},
        @{Name='FreePct'; Expression={[math]::Round($_.FreeSpace/$_.Size*100,1)}}

$topProc = Get-Process | Sort-Object WorkingSet64 -Descending |
    Select-Object -First 5 Name, Id,
        @{Name='RAM_MB'; Expression={[math]::Round($_.WorkingSet64/1MB)}}

$stoppedSvc = Get-Service | Where-Object {
    $_.StartType -eq 'Automatic' -and $_.Status -ne 'Running'
} | Select-Object Name, DisplayName, Status

$failedEvents = Get-WinEvent -FilterHashtable @{
    LogName='System'; Level=1,2; StartTime=(Get-Date).AddHours(-24)
} -MaxEvents 10 -ErrorAction SilentlyContinue |
    Select-Object TimeCreated, Id, ProviderName,
        @{Name='Msg'; Expression={$_.Message.Substring(0, [Math]::Min(100, $_.Message.Length))}}

$css = @'

'@

$html = @"
$css

Ежедневный отчёт: $serverName

Дата: $reportDate | Uptime: $($uptimeSpan.Days)д $($uptimeSpan.Hours)ч

Дисковое пространство

$($disks | ConvertTo-Html -Fragment | Out-String)

Топ-5 процессов по RAM

$($topProc | ConvertTo-Html -Fragment | Out-String)

Остановленные автозапускаемые службы

$(if ($stoppedSvc) { $stoppedSvc | ConvertTo-Html -Fragment | Out-String } else { '

Все автозапускаемые службы работают

' })

Критичные события за 24 часа

$(if ($failedEvents) { $failedEvents | ConvertTo-Html -Fragment | Out-String } else { '

Критичных событий нет

' })

Автоматический отчёт — система мониторинга АйТи Фреш

"@ $filePath = "$ReportPath\DailyReport_$(Get-Date -Format 'yyyyMMdd').html" $html | Out-File -FilePath $filePath -Encoding UTF8 Send-MailMessage -From "report@$serverName" -To $AdminEmail ` -Subject "Ежедневный отчёт: $serverName — $reportDate" ` -Body $html -BodyAsHtml -SmtpServer $SmtpServer -Encoding UTF8 Write-Host "Отчёт отправлен на $AdminEmail" -ForegroundColor Green

Результаты внедрения

Проект автоматизации IT-рутины для страховой компании был завершён за 2 недели. Вот что было достигнуто:

  • 20 PowerShell-скриптов внедрены и настроены на автоматический запуск через Task Scheduler
  • Экономия 15 часов в неделю — время администратора, ранее потраченное на рутинные проверки
  • Email-уведомления о всех критичных событиях в течение 5 минут — вместо обнаружения проблем «на следующий день»
  • Ежедневный HTML-отчёт для руководства — полная прозрачность состояния IT-инфраструктуры
  • Аудит безопасности AD — выявлено 47 неактивных учёток (некоторые с привилегированным доступом), все деактивированы
  • Автоматический бэкап файлов и конфигурации сетевого оборудования — с ротацией и уведомлениями
  • Документация — каждый скрипт снабжён комментариями, администратор клиента может самостоятельно модифицировать параметры

Бизнес-результат: за первый месяц после внедрения администратор перенаправил высвободившееся время на миграцию почтового сервера и внедрение VPN — проекты, которые откладывались полгода. Руководство получило инструмент контроля за IT-инфраструктурой, а компания успешно прошла аудит безопасности регулятора.

Часто задаваемые вопросы

Мы используем Task Scheduler (Планировщик задач). Создаётся задача с действием: программа — powershell.exe, аргументы — -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\YourScript.ps1. Задача запускается от имени учётной записи с достаточными правами. Для скриптов, работающих с AD, наши специалисты используют сервисную учётную запись. Можно также создавать задачи через PowerShell командой Register-ScheduledTask.

Специалисты АйТи Фреш никогда не хранят пароли в открытом виде в скриптах. Мы используем Export-Clixml для сохранения зашифрованных credentials: Get-Credential | Export-Clixml C:\Scripts\cred.xml. Файл зашифрован DPAPI и доступен только тому пользователю, который его создал, и только на том же компьютере. Для продвинутых сценариев рекомендуем Windows Credential Manager или Azure Key Vault.

Мы используем командлет Send-MailMessage с параметрами SMTP-сервера. Для HTML-писем добавляется флаг -BodyAsHtml. Если SMTP-сервер требует аутентификацию, передаётся -Credential (Get-Credential). Для Office 365 используется -SmtpServer smtp.office365.com -Port 587 -UseSsl. Обратите внимание: Send-MailMessage помечен как устаревший в PowerShell 7. Альтернатива — модуль Mailozaurr или .NET-класс System.Net.Mail.SmtpClient.

Мы используем PowerShell Remoting через Invoke-Command: Invoke-Command -ComputerName SRV01 -FilePath C:\Scripts\script.ps1. Предварительно на целевом сервере нужно выполнить Enable-PSRemoting -Force. Для постоянных сессий используется New-PSSession. Если серверы не в домене, добавляются в TrustedHosts: Set-Item WSMan:\localhost\Client\TrustedHosts -Value 'SRV01'. Для массового выполнения передаётся массив серверов в параметр -ComputerName.

Для Windows-инфраструктуры наши специалисты рекомендуют PowerShell 5.1, встроенный в Windows Server 2016/2019/2022. Он полностью совместим с модулями Active Directory, Exchange, SCCM и другими Microsoft-продуктами. PowerShell 7 (кроссплатформенный) используйте для новых проектов и там, где нужны улучшения синтаксиса. Оба можно установить параллельно на одном сервере.

Нужна помощь с настройкой?

Специалисты АйТи Фреш помогут с внедрением и настройкой — 15+ лет опыта, обслуживание от 15 000 ₽/мес

📞 Связаться с нами
#PowerShell скрипты администратора#PowerShell автоматизация#PowerShell Active Directory#мониторинг дисков PowerShell#PowerShell бэкап скрипт#PowerShell email уведомления#PowerShell журналы событий#PowerShell сетевая диагностика