RDP sunucu bağlantı geçmişi: PowerShell ile kim, ne zaman ve hangi IP'den bağlandı raporu
RDP sunucu üzerinde kimin, ne zaman ve hangi IP'den bağlandığını olay günlüklerinden PowerShell ile çıkarın. Günlük boyutu ve denetim ilkesi ayarı, oturum süresi hesabı, CSV/HTML rapor, mesai dışı ve bilinmeyen IP işaretleme ve haftalık e-posta raporu tek rehberde.
Batuhan Çelikel14 dk okuma
Logo, Mikro ya da başka bir iş yazılımını çalıştıran bir RDP sunucu üzerinde "dün gece kim bağlandı, hangi IP'den geldi, ne kadar içeride kaldı" sorusunun yanıtı olay günlüklerinde durur. Sorun, bu bilginin birkaç ayrı günlüğe dağılması, varsayılan günlük boyutlarının küçük kalması ve olay metninin sistem diline göre değişmesidir. Bu rehber sistem yöneticileri ve kendi sunucusunu yöneten KOBİ sahipleri için hazırlandı: önce izlerin kaybolmamasını sağlayacak ayarları yapacak, sonra PowerShell ile bağlantı geçmişini CSV ve HTML rapora dönüştürecek, en sonda bu raporu haftalık e-posta olarak alacaksınız.
Ön koşullar
- Windows Server 2016, 2019, 2022 ya da 2025 ve yerleşik Windows PowerShell 5.1.
- Yerel yönetici yetkisi. Security günlüğünü okumak yönetici ister.
- Betikleri kaydedeceğiniz bir klasör. Örneklerde
C:\RdpRaporkullanılıyor. - Betik dosyalarını UTF-8 (BOM'lu) kaydedin. PowerShell 5.1, BOM'suz UTF-8 dosyadaki Türkçe karakterleri bozuk okur ve "ç", "ı" içeren sütun adları anlamsız görünür.
RDP sunucu bağlantı izleri hangi günlüklerde tutulur?
Tek bir RDP bağlantısı birkaç olay üretir. Hangi soruyu hangi olayın yanıtladığını bilmek, raporu doğru kurmanın ilk adımıdır.
| Olay | Günlük | Ne anlatır |
|---|---|---|
| 1149 | TerminalServices-RemoteConnectionManager/Operational | Ağ düzeyinde kimlik doğrulama başarılı. Kullanıcı adı ve kaynak IP içerir. Henüz oturum açıldığı anlamına gelmez. |
| 4624, LogonType 10 | Security | Uzak etkileşimli oturum açıldı. Kullanıcı, kaynak IP ve oturum kimliği (LogonId) içerir. |
| 21 | TerminalServices-LocalSessionManager/Operational | Oturum açma başarılı. Kullanıcı, oturum numarası ve kaynak adres içerir. |
| 23 | LocalSessionManager/Operational | Oturum kapatıldı (logoff). |
| 24 | LocalSessionManager/Operational | Oturumun bağlantısı koptu ya da kullanıcı pencereyi kapattı (disconnect). |
| 25 | LocalSessionManager/Operational | Kopuk oturuma yeniden bağlanıldı. Yeni kaynak IP burada görünür. |
| 4778 / 4779 | Security | Oturuma yeniden bağlanma / bağlantı kesilmesi, istemci adı ve adresiyle. |
Rapor için en kullanışlı kaynak LocalSessionManager günlüğüdür: 21 ve 25 bağlantının başını, 23 ve 24 sonunu verir, hepsi aynı oturum numarasını taşır. 4624 olayı ise Security günlüğünde durduğu için silinmeye ve değiştirilmeye karşı daha korunaklıdır; çapraz kontrol için kullanılır. 1149 olayı ise kimlerin kapıya kadar gelip doğru parolayla doğrulandığını gösterir. Başarısız denemeler (4625) ayrı bir konudur; onlar için RDP brute force engelleme rehberindeki 4625 raporunu kullanabilirsiniz.
Olay şemaları için Microsoft'un 4624 olay açıklaması alan adlarını tek tek listeler.
Adım 1: Günlük boyutunu ve denetim ilkesini ayarlama
Geçmişi raporlamanın önündeki en büyük engel, günlüğün dolup en eski kayıtların üzerine yazılmasıdır. Operational günlüklerin varsayılan boyutu küçüktür; kalabalık bir sunucuda birkaç günlük kayıt bile sığmayabilir. Önce mevcut durumu görün:
# Üç günlüğün mevcut boyutunu, kayıt sayısını ve en eski kaydın tarihini gösterir
'Security',
'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational',
'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational' | ForEach-Object {
$log = Get-WinEvent -ListLog $_
$eski = Get-WinEvent -LogName $_ -MaxEvents 1 -Oldest -ErrorAction SilentlyContinue
[pscustomobject]@{
Gunluk = $_.Split('/')[0] -replace 'Microsoft-Windows-TerminalServices-', ''
BoyutMB = [math]::Round($log.MaximumSizeInBytes / 1MB)
KayitSayisi = $log.RecordCount
EnEskiKayit = if ($eski) { $eski.TimeCreated } else { 'yok' }
}
} | Format-Table -AutoSize
Karar ölçütü basit: "En eski kayıt" sütunu, raporlamak istediğiniz dönemden (örneğin 90 gün) daha yeniyse günlük küçüktür.
Aşağıdaki betik Windows Server 2016 ile 2025 arası ve PowerShell 5.1 için hazırlandı. Üç günlüğü hedef boyuta büyütür (zaten büyükse dokunmaz), eski değerleri eski-ayarlar.csv dosyasına bir kez kaydeder ve oturum açma, oturum kapatma ile "diğer oturum olayları" denetimini açar. Denetim alt kategorileri GUID ile verildiği için Türkçe ve İngilizce sistemde aynı şekilde çalışır. Tekrar çalıştırmak güvenlidir.
#Requires -RunAsAdministrator
# RDP izlerini tutan günlükleri büyütür ve oturum denetimini açar.
# Windows Server 2016/2019/2022/2025, Windows PowerShell 5.1 için hazırlandı.
# Geri alma: eski-ayarlar.csv'deki boyutları geri yazın, denetim için
# auditpol /restore /file:C:\RdpRapor\auditpol-yedek.csv
$Klasor = 'C:\RdpRapor'
# Hedef boyutlar (MB). Disk alanınıza göre değiştirin.
$HedefMB = @{
'Security' = 1024
'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational' = 256
'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational' = 256
}
New-Item -ItemType Directory -Path $Klasor -Force | Out-Null
$yedek = Join-Path $Klasor 'eski-ayarlar.csv'
foreach ($ad in $HedefMB.Keys) {
$log = Get-WinEvent -ListLog $ad
# Eski değeri yalnız ilk çalıştırmada kaydet
$kayitliMi = (Test-Path $yedek) -and (Import-Csv $yedek | Where-Object { $_.LogName -eq $ad })
if (-not $kayitliMi) {
[pscustomobject]@{ LogName = $ad; MaximumSizeInBytes = $log.MaximumSizeInBytes } |
Export-Csv $yedek -Append -NoTypeInformation -Encoding UTF8
}
$hedef = [int64]$HedefMB[$ad] * 1MB
$degisti = $false
if ($log.MaximumSizeInBytes -lt $hedef) { $log.MaximumSizeInBytes = $hedef; $degisti = $true }
if (-not $log.IsEnabled) { $log.IsEnabled = $true; $degisti = $true }
if ($degisti) { $log.SaveChanges(); Write-Host "Güncellendi: $ad" }
else { Write-Host "Değişiklik gerekmedi: $ad" }
}
# Denetim ilkesini yedekle (yalnız ilk kez)
$auditYedek = Join-Path $Klasor 'auditpol-yedek.csv'
if (-not (Test-Path $auditYedek)) { auditpol /backup /file:$auditYedek | Out-Null }
# Logon, Logoff, Other Logon/Logoff Events (dil bağımsız GUID'ler)
$altKategoriler = '{0CCE9215-69AE-11D9-BED3-505055000000}',
'{0CCE9216-69AE-11D9-BED3-505055000000}',
'{0CCE921C-69AE-11D9-BED3-505055000000}'
foreach ($g in $altKategoriler) {
auditpol /set /subcategory:$g /success:enable /failure:enable | Out-Null
}
# Doğrulama
auditpol /get /subcategory:($altKategoriler -join ',')
Son komutun çıktısında üç satırın da "Success and Failure" (Türkçe sistemde "Başarılı ve Başarısız") göstermesi gerekir. Sunucu bir etki alanına bağlıysa ve Grup İlkesi denetim ayarlarını yönetiyorsa yerel değişikliğiniz bir sonraki ilke yenilemesinde ezilir; o durumda ayarı Grup İlkesi'nde yapın. auditpol seçenekleri için resmi auditpol set belgesine bakabilirsiniz.
Adım 2: Hızlı kontrol için tek seferlik sorgu
Rapor betiğine geçmeden önce Security günlüğünde son RDP oturumlarını görmek isterseniz şu sorgu yeterli. Mesaj metnini değil olayın XML alanlarını okuduğu için sistem dilinden etkilenmez.
# Son 50 uzak etkileşimli oturum açma (4624, LogonType 10)
$xpath = "*[System[(EventID=4624)]] and *[EventData[Data[@Name='LogonType']='10']]"
Get-WinEvent -LogName Security -FilterXPath $xpath -MaxEvents 50 -ErrorAction SilentlyContinue |
ForEach-Object {
$alan = @{}
([xml]$_.ToXml()).Event.EventData.Data | ForEach-Object { $alan[$_.Name] = $_.'#text' }
[pscustomobject]@{
Zaman = $_.TimeCreated
Kullanici = "$($alan.TargetDomainName)\$($alan.TargetUserName)"
KaynakIP = $alan.IpAddress
LogonId = $alan.TargetLogonId
}
} | Format-Table -AutoSize
Beklenen çıktı, her satırda zaman, SUNUCUADI\kullanici biçiminde hesap ve 192.0.2.10 gibi bir kaynak adrestir. Hiç satır gelmiyorsa ya denetim kapalıydı ya da günlük o dönemi çoktan ezmiştir; Adım 1'e dönün.
Adım 3: PowerShell ile RDP sunucu bağlantı raporu
Asıl betik LocalSessionManager olaylarını zaman sırasına dizer ve her bağlantıyı bir satıra çevirir: başlangıç 21 ya da 25, bitiş 24 ya da 23. Aynı oturum numarası zamanla yeniden kullanıldığı için betik açık bağlantıları oturum numarasına göre izler ve yeni bir başlangıç geldiğinde öncekini kapatır. Ardından her satıra süre, mesai dışı ve izinli ağ dışı IP işaretlerini ekler, kullanıcı bazlı özet çıkarır ve 1149 olaylarından kaynak IP özeti üretir.
Betik Windows Server 2016 ile 2025 arası ve PowerShell 5.1 için hazırlandı. Yalnızca olay günlüğünü okur ve CiktiKlasoru altına dosya yazar; sistemde ayar değiştirmez. Geri almak için oluşturduğu rapor klasörünü silmeniz yeterlidir. Kendinize göre değiştirmeniz gereken değerler: IzinliAglar (ofis ve VPN ağınız, örnekteki 198.51.100.0/24 yerine), mesai saatleri ve SMTP bilgileri.
C:\RdpRapor\RdpRapor.ps1 olarak kaydedin:
#Requires -RunAsAdministrator
# RDP bağlantı geçmişi raporu: kim, ne zaman, hangi IP'den, ne kadar süre.
# Windows Server 2016/2019/2022/2025, Windows PowerShell 5.1 için hazırlandı.
# Yalnız okuma yapar; rapor dosyalarını CiktiKlasoru altına yazar.
[CmdletBinding()]
param(
[int]$Gun = 7,
[string]$CiktiKlasoru = 'C:\RdpRapor\Raporlar',
# Bilinen ağlar: yerel ağlar + ofis/VPN çıkışınız (ORNEK: 198.51.100.0/24)
[string[]]$IzinliAglar = @('10.0.0.0/8','172.16.0.0/12','192.168.0.0/16','198.51.100.0/24'),
[int]$MesaiBaslangic = 8, # 08:00 öncesi mesai dışı
[int]$MesaiBitis = 19, # 19:00 ve sonrası mesai dışı
[switch]$EpostaGonder,
[string]$SmtpSunucu = 'smtp.example.com',
[int]$SmtpPort = 587,
[string]$Gonderen = 'rapor@example.com',
[string[]]$Alicilar = @('bilgiislem@example.com'),
[string]$KimlikDosyasi = 'C:\RdpRapor\smtp-kimlik.xml'
)
$ErrorActionPreference = 'Stop'
$baslangic = (Get-Date).Date.AddDays(-$Gun)
New-Item -ItemType Directory -Path $CiktiKlasoru -Force | Out-Null
# --- Yardımcı: IPv4 adresi verilen CIDR bloğunda mı? ---
function Test-IPv4InCidr {
param([string]$Ip, [string]$Cidr)
$ipObj = $null
if (-not [System.Net.IPAddress]::TryParse($Ip, [ref]$ipObj)) { return $false }
if ($ipObj.AddressFamily -ne 'InterNetwork') { return $false }
$ag, $bit = $Cidr.Split('/')
$bit = [int]$bit
if ($bit -eq 0) { return $true }
$ipB = $ipObj.GetAddressBytes(); [Array]::Reverse($ipB)
$agB = ([System.Net.IPAddress]::Parse($ag)).GetAddressBytes(); [Array]::Reverse($agB)
$maske = [uint32]([math]::Pow(2, 32) - [math]::Pow(2, 32 - $bit))
return (([BitConverter]::ToUInt32($ipB, 0) -band $maske) -eq ([BitConverter]::ToUInt32($agB, 0) -band $maske))
}
function Get-IpDurumu {
param([string]$Ip)
if ([string]::IsNullOrWhiteSpace($Ip) -or $Ip -eq 'LOCAL') { return 'Yerel konsol' }
foreach ($c in $IzinliAglar) { if (Test-IPv4InCidr -Ip $Ip -Cidr $c) { return 'İzinli ağ' } }
return 'Bilinmeyen'
}
# --- 1) Oturum olaylarını oku (21,23,24,25) ve eskiden yeniye sırala ---
$lsm = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'
$olaylar = @(Get-WinEvent -FilterHashtable @{ LogName = $lsm; Id = 21,23,24,25; StartTime = $baslangic } `
-ErrorAction SilentlyContinue | Sort-Object TimeCreated)
$acik = @{} # OturumNo -> henüz kapanmamış bağlantı
$kayitlar = New-Object System.Collections.Generic.List[object]
foreach ($e in $olaylar) {
$d = ([xml]$e.ToXml()).Event.UserData.EventXML # dil bağımsız alanlar
$sid = [string]$d.SessionID
if ($e.Id -eq 21 -or $e.Id -eq 25) {
# Aynı oturum numarasında kapanmamış kayıt kaldıysa onu bitişsiz kapat
if ($acik.ContainsKey($sid)) { $kayitlar.Add($acik[$sid]); $acik.Remove($sid) }
$tur = 'Yeni oturum'
if ($e.Id -eq 25) { $tur = 'Yeniden bağlanma' }
$acik[$sid] = [pscustomobject]@{
Kullanici = [string]$d.User
OturumNo = $sid
KaynakIP = [string]$d.Address
Tur = $tur
Baslangic = $e.TimeCreated
Bitis = $null
BitisTuru = 'Kayıt yok'
}
}
elseif ($acik.ContainsKey($sid)) {
$acik[$sid].Bitis = $e.TimeCreated
if ($e.Id -eq 24) { $acik[$sid].BitisTuru = 'Bağlantı koptu' }
else { $acik[$sid].BitisTuru = 'Oturum kapatıldı' }
$kayitlar.Add($acik[$sid]); $acik.Remove($sid)
}
}
foreach ($k in $acik.Values) { $k.BitisTuru = 'Hâlâ bağlı'; $kayitlar.Add($k) }
# --- 2) Süre ve uyarı işaretleri ---
$rapor = foreach ($k in ($kayitlar | Sort-Object Baslangic)) {
$sure = ''
if ($k.Bitis) { $sure = [math]::Round(($k.Bitis - $k.Baslangic).TotalMinutes) }
$saat = $k.Baslangic.Hour
$haftaSonu = $k.Baslangic.DayOfWeek -in 'Saturday', 'Sunday'
$mesaiDisi = $haftaSonu -or $saat -lt $MesaiBaslangic -or $saat -ge $MesaiBitis
$ipDurumu = Get-IpDurumu -Ip $k.KaynakIP
$uyarilar = @()
if ($mesaiDisi) { $uyarilar += 'Mesai dışı' }
if ($ipDurumu -eq 'Bilinmeyen') { $uyarilar += 'Bilinmeyen IP' }
[pscustomobject]@{
Kullanici = $k.Kullanici
KaynakIP = $k.KaynakIP
IpDurumu = $ipDurumu
Tur = $k.Tur
Baslangic = $k.Baslangic.ToString('yyyy-MM-dd HH:mm')
Bitis = if ($k.Bitis) { $k.Bitis.ToString('yyyy-MM-dd HH:mm') } else { '' }
BitisTuru = $k.BitisTuru
SureDakika = $sure
Uyari = ($uyarilar -join ', ')
}
}
$rapor = @($rapor)
$uyarili = @($rapor | Where-Object { $_.Uyari })
# --- 3) Kullanıcı bazlı özet ---
$ozet = $rapor | Group-Object Kullanici | ForEach-Object {
[pscustomobject]@{
Kullanici = $_.Name
Baglanti = $_.Count
ToplamDakika = ($_.Group | Where-Object { $_.SureDakika -ne '' } |
Measure-Object SureDakika -Sum).Sum
FarkliIP = (($_.Group.KaynakIP | Sort-Object -Unique) -join ', ')
UyariSayisi = @($_.Group | Where-Object { $_.Uyari }).Count
}
} | Sort-Object Baglanti -Descending
# --- 4) 1149: ağ düzeyinde doğrulanan kaynak IP özeti ---
$rcm = 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational'
$dogrulama = @(Get-WinEvent -FilterHashtable @{ LogName = $rcm; Id = 1149; StartTime = $baslangic } `
-ErrorAction SilentlyContinue) | ForEach-Object {
$d = ([xml]$_.ToXml()).Event.UserData.EventXML
[pscustomobject]@{ Kullanici = [string]$d.Param1; KaynakIP = [string]$d.Param3 }
} | Group-Object KaynakIP, Kullanici | ForEach-Object {
[pscustomobject]@{ KaynakIP_Kullanici = $_.Name; Adet = $_.Count }
} | Sort-Object Adet -Descending
# --- 5) CSV ve HTML çıktısı ---
$tarih = Get-Date -Format 'yyyy-MM-dd'
$csvYolu = Join-Path $CiktiKlasoru "rdp-rapor-$tarih.csv"
$htmlYolu = Join-Path $CiktiKlasoru "rdp-rapor-$tarih.html"
# Noktalı virgül ayırıcı: Türkçe Excel doğrudan sütunlara böler
$rapor | Export-Csv $csvYolu -Delimiter ';' -NoTypeInformation -Encoding UTF8
$css = @'
<style>
body { font-family: Segoe UI, Arial, sans-serif; font-size: 13px; }
table { border-collapse: collapse; margin-bottom: 20px; }
th, td { border: 1px solid #ccc; padding: 4px 8px; }
th { background: #eee; }
h3.uyari { color: #b00020; }
</style>
'@
$govde = "<h2>RDP bağlantı raporu: $env:COMPUTERNAME</h2>"
$govde += "<p>Dönem: $($baslangic.ToString('yyyy-MM-dd')) - $tarih. Bağlantı: $($rapor.Count). Uyarılı: $($uyarili.Count).</p>"
if ($uyarili.Count -gt 0) {
$govde += '<h3 class="uyari">Dikkat gerektiren bağlantılar</h3>'
$govde += ($uyarili | ConvertTo-Html -Fragment | Out-String)
}
$govde += '<h3>Kullanıcı özeti</h3>' + ($ozet | ConvertTo-Html -Fragment | Out-String)
$govde += '<h3>1149 doğrulama özeti</h3>' + ($dogrulama | ConvertTo-Html -Fragment | Out-String)
$govde += '<h3>Tüm bağlantılar</h3>' + ($rapor | ConvertTo-Html -Fragment | Out-String)
$html = ConvertTo-Html -Head $css -Body $govde -Title "RDP rapor $tarih" | Out-String
Set-Content -Path $htmlYolu -Value $html -Encoding UTF8
Write-Host "Rapor: $htmlYolu"
Write-Host "Bağlantı: $($rapor.Count), uyarılı: $($uyarili.Count)"
# --- 6) İsteğe bağlı e-posta ---
if ($EpostaGonder) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$kimlik = Import-Clixml -Path $KimlikDosyasi
$konu = "[$env:COMPUTERNAME] RDP raporu $tarih - uyarılı: $($uyarili.Count)"
Send-MailMessage -SmtpServer $SmtpSunucu -Port $SmtpPort -UseSsl -Credential $kimlik `
-From $Gonderen -To $Alicilar -Subject $konu -Body $html -BodyAsHtml `
-Attachments $csvYolu -Encoding ([System.Text.Encoding]::UTF8)
Write-Host 'E-posta gönderildi.'
}
Önce e-postasız çalıştırın:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\RdpRapor\RdpRapor.ps1 -Gun 7
Çıktı iki satırdır: HTML raporun yolu ve "Bağlantı: N, uyarılı: M" özeti. HTML dosyasını tarayıcıda açın. Olay alanlarının nasıl filtrelendiğini merak ederseniz Get-WinEvent belgesi FilterHashtable anahtarlarını açıklar.
Raporu okurken nelere bakmalı?
- Bilinmeyen IP + mesai dışı birlikteyse ilk incelenecek satırdır. Kullanıcıya sorun; tanımıyorsa parolasını hemen değiştirin ve aynı zaman aralığında 4624 kayıtlarına bakın.
- Aynı kullanıcı, kısa aralıkta farklı IP'ler: kullanıcı ofisten eve geçmiş olabilir ya da hesabı paylaşılıyordur. Paylaşılan hesap hem lisans hem denetim açısından sorundur.
- "Hâlâ bağlı" ya da çok uzun süreler: kopuk bırakılan oturumlar bellek tüketir. Bunları kapatmak için RDP oturum yönetimi betiğini kullanabilirsiniz.
- 1149 var, 21 yok: kişi parolayı doğru girmiş ama oturum açılmamış. Lisans hatası, oturum sınırı ya da kullanıcının vazgeçmesi olabilir.
Adım 4: Haftalık raporu zamanlanmış görevle gönderme
E-posta için SMTP kimlik bilgisini düz metin olarak betiğe yazmayın. Export-Clixml, parolayı Windows DPAPI ile yalnızca o kullanıcı ve o makinede çözülebilecek şekilde şifreler. Bu yüzden dosyayı, görevi çalıştıracak hesapla oturum açmışken oluşturmalısınız. Uygulama ya da site e-postası için ayrı bir gönderim hesabı kullanmak isterseniz SMTP hizmeti bu iş için uygundur.
Aşağıdaki betik Windows Server 2016 ile 2025 arası ve PowerShell 5.1 için hazırlandı. SMTP kimliğini kaydeder ve her pazartesi 07:30'da çalışan görevi oluşturur; -Force sayesinde tekrar çalıştırıldığında mevcut görevi günceller. Geri almak için Unregister-ScheduledTask -TaskName 'RDP-Haftalik-Rapor' -Confirm:$false komutunu çalıştırın ve smtp-kimlik.xml dosyasını silin. ORNEK_RAPOR_HESABI, yerel yönetici grubunda olan ayrı bir hesaptır.
#Requires -RunAsAdministrator
# Bu betiği ORNEK_RAPOR_HESABI ile oturum açmışken çalıştırın.
# 1) SMTP kimliğini bu hesaba özel şifreli dosyaya yaz
$kimlikDosyasi = 'C:\RdpRapor\smtp-kimlik.xml'
if (-not (Test-Path $kimlikDosyasi)) {
Get-Credential -Message 'SMTP kullanıcı adı ve parolası' | Export-Clixml -Path $kimlikDosyasi
}
# 2) Görev tanımı
$eylem = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\RdpRapor\RdpRapor.ps1" -Gun 7 -EpostaGonder'
$tetik = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At '07:30'
$ayar = New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 30)
# 3) Görevi kullanıcı oturumu kapalıyken de çalışacak şekilde kaydet
$hesap = Get-Credential -UserName "$env:COMPUTERNAME\ORNEK_RAPOR_HESABI" -Message 'Görev hesabı'
Register-ScheduledTask -TaskName 'RDP-Haftalik-Rapor' -Action $eylem -Trigger $tetik -Settings $ayar `
-User $hesap.UserName -Password $hesap.GetNetworkCredential().Password -RunLevel Highest -Force
# 4) Hemen bir kez deneyin ve sonucu kontrol edin
Start-ScheduledTask -TaskName 'RDP-Haftalik-Rapor'
Start-Sleep -Seconds 60
Get-ScheduledTaskInfo -TaskName 'RDP-Haftalik-Rapor' | Select-Object LastRunTime, LastTaskResult
LastTaskResult değeri 0 ise görev başarıyla bitmiştir ve gelen kutunuzda rapor olmalıdır. Farklı bir sayı görürseniz betiği aynı hesapla elle çalıştırıp hata mesajını okuyun; en sık neden kimlik dosyasının başka bir hesapla oluşturulmuş olmasıdır.
Send-MailMessage Microsoft tarafından artık önerilmeyen bir komut olarak işaretlidir ama Windows PowerShell 5.1'de çalışmaya devam eder. Kurumunuz farklı bir gönderim yöntemi kullanıyorsa yalnız 6. bölümü değiştirmeniz yeterlidir.
Sık yapılan hatalar
Message alanına göre filtrelemek. $_.Message -match 'Kullanıcı:' gibi bir filtre İngilizce sistemde hiç eşleşmez. Bazı sistemlerde, bölge ayarı ile kurulu dil paketi uyuşmadığında Message alanı tamamen boş da gelebilir. Betikteki gibi ToXml() ile alan adlarını okuyun; bu yöntem dil bağımsızdır.
Günlüğün sessizce ezilmesi. Rapor yalnız günlükte duran dönemi kapsar. Adım 1'deki "en eski kayıt" kontrolünü ayda bir tekrarlayın. Uzun saklama gerekiyorsa HTML ve CSV raporlarını ayrı bir yere arşivleyin; olay günlüğünü sonsuz büyütmek çözüm değildir.
Ağ geçidi ya da VPN arkasındaki IP'yi kullanıcı IP'si sanmak. Kullanıcılar RD Gateway, bir VPN ya da NAT yapan güvenlik duvarı üzerinden geliyorsa olaylarda görünen adres o aygıtın adresidir. Gerçek kaynak için ağ geçidinin ya da VPN sunucusunun kendi günlüklerine bakmak gerekir.
Oturum numarasını kalıcı kimlik sanmak. Oturum 3 bugün bir kullanıcıya, yarın başka birine ait olabilir. Betik bu yüzden olayları zaman sırasıyla eşleştirir; elle inceleme yaparken de kullanıcı adını ve zamanı birlikte okuyun.
Betiği BOM'suz kaydetmek. Notepad'de "UTF-8 with BOM" seçmezseniz PowerShell 5.1 Türkçe karakterli dizeleri bozar ve bazen ayrıştırma hatası verir.
Denetimi açmadan 4624 aramak. Denetim kapalı iken oluşmamış olay sonradan üretilemez. Ayarı bugün açarsanız geçmişe dönük değil, bugünden sonraki bağlantılar için kayıt oluşur.
KVKK açısından erişim kayıtları
Kullanıcı adı, IP adresi ve oturum saatleri kişisel veridir. Bu kayıtları tutmak, yetkisiz erişimi tespit etmek için KVKK'nın veri güvenliği yükümlülüğüyle uyumlu bir tedbirdir; ancak kayıtların kendisi de korunmalıdır:
- Rapor klasörüne erişimi yalnız yöneticilerle sınırlayın (NTFS izinleri).
- Saklama süresini şirketinizin kişisel veri saklama ve imha politikasında yazılı olarak belirleyin ve rapor arşivini bu süreye göre temizleyin.
- Raporu e-postayla gönderiyorsanız alıcı listesini dar tutun; tüm çalışanların bulunduğu bir dağıtım listesine göndermeyin.
- Çalışanları bağlantı kayıtlarının tutulduğu konusunda aydınlatma metniyle bilgilendirin.
Kesin süre ve kapsam için hukuk danışmanınızın görüşünü alın; teknik tarafın görevi kaydın eksiksiz, değiştirilemez ve erişimi sınırlı tutulmasını sağlamaktır.
Sık sorulan sorular
Raporda kullanıcı adı var ama IP adresi boş ya da "LOCAL" görünüyor, neden?
"LOCAL" ya da boş adres, oturumun konsoldan ya da hipervizör konsolundan açıldığını gösterir. Betik bu satırları "Yerel konsol" olarak işaretler. Bulut sunucuda sağlayıcı panelindeki konsolu kullandığınızda da bu kayıt oluşur.
Silinen günlükleri betik geri getirebilir mi?
Hayır. Betik yalnız günlükte duran kayıtları okur. Günlüğün temizlenmesi ise Security günlüğünde 1102 olayı bırakır; bu olayın varlığı, birinin izleri silmeye çalıştığına işaret eder ve ayrıca kontrol edilmelidir.
Yabancı ülkeden gelen bağlantıları nasıl ayırırım?
Betik ülke bilgisi çıkarmaz; bunun yerine izinli ağ listenizin dışındaki her adresi "Bilinmeyen" olarak işaretler. Bu yaklaşım dış bir servise IP göndermeden çalışır. Daha sağlam çözüm, 3389'u internete hiç açmamak ve erişimi VPN ya da IP izin listesiyle sınırlamaktır.
Birden fazla RDP sunucusu için tek rapor alabilir miyim?
Evet. Betiği her sunucuda zamanlanmış görev olarak çalıştırıp CSV dosyalarını ortak bir paylaşıma yazabilir, sonra Import-Csv ile birleştirebilirsiniz. CSV'ye sunucu adı sütunu eklemek için çıktı nesnesine Sunucu = $env:COMPUTERNAME satırını ekleyin.
Bu rapor brute force saldırılarını da gösterir mi?
Hayır. Rapor başarılı bağlantıları listeler. Başarısız parola denemeleri 4625 olayında tutulur ve ayrı bir analiz ister.
Sonuç
RDP sunucu bağlantı geçmişini güvenilir biçimde raporlamak üç adıma iner: günlüklerin ezilmeyecek boyutta olması ve denetimin açık olması, olayların mesaj metniyle değil XML alanlarıyla okunması ve sonucun düzenli olarak bir insanın önüne gelmesi. Haftalık raporda "Bilinmeyen IP" ve "Mesai dışı" sütunlarına bakmak birkaç dakika sürer ve yetkisiz erişimi erken fark etmenizi sağlar. Ekibiniz ofis dışından aynı masaüstüne bağlanıyorsa erişim mimarisini tasarlarken uzak masaüstü çözümü sayfasındaki seçeneklere de göz atabilirsiniz.
- #rdp
- #uzak masaüstü
- #powershell
- #olay günlüğü
- #windows server
- #denetim
- #kvkk
Yazar
Kurucu, sunucu.com.tr · Bulut altyapısı ve yapay zeka otomasyonları
sunucu.com.tr ve Sunucu UK'in kurucusu. Bulut altyapısı, sanallaştırma ve sunucu yönetimi üzerine çalışıyor; şirketlerin iş yazılımlarını, web sitelerini ve yapay zeka iş yüklerini güvenli sunuculara taşıyor. Blogdaki rehberler sahada karşılaşılan kurulum, güvenlik ve taşıma işlerinden derlenir; yazılar yapay zeka desteğiyle hazırlanır ve kaynaklarla doğrulanır.