#Requires -Version 5.1 <# =============================================================================== CİHAZ ENVANTER TOPLAMA SCRIPTI IT Danışmanlık - saha envanter çıkarma aracı KULLANIM: powershell -ExecutionPolicy Bypass -File .\Envanter-Topla.ps1 YÖNETİCİ olarak çalıştırılması önerilir (BitLocker, TPM, disk sağlığı, Secure Boot bilgileri için gerekir). Yönetici değilse script yine çalışır, sadece o bölümler "yetki gerekli" olarak işaretlenir. Script SALT-OKUNURDUR. Cihazda hiçbir ayarı değiştirmez, sadece okur. ÇIKTILAR (script ile aynı klasöre yazılır): Envanter\_.html -> okunabilir cihaz raporu Envanter\_.json -> ham veri (arşiv / işlem için) Envanter\_TUM-CIHAZLAR.csv -> her cihazdan 1 satır, birikimli ÖNEMLİ: Bu dosya UTF-8 BOM ile kaydedilmelidir. BOM olmadan PowerShell 5.1 dosyayı ANSI okur ve Türkçe karakterler bozulur. =============================================================================== #> [CmdletBinding()] param( # Çıktıların yazılacağı klasör. Boş bırakılırsa scriptin yanındaki Envanter klasörü. [string]$CiktiKlasoru, # Kurulu yazılım listesini atla (çok hızlı tarama için) [switch]$YazilimAtla, # Rapor bittiğinde HTML dosyasını otomatik açma [switch]$Acma, # ---- PAYLAŞIM ALICISI ---------------------------------------------- # Raporun "Raporu Paylaş" panelinde ön tanımlı olacak alıcı bilgileri. # Buraya kendi bilgilerinizi yazarsanız sahadaki teknisyen hiçbir şey # yazmadan doğrudan size gönderebilir. [string]$PaylasimAd = 'IT Danışmanlık', [string]$PaylasimTelefon = '', # ör. 905321234567 (başında + ve boşluk olmadan) [string]$PaylasimEposta = '' # ör. ornek@ornek.com ) # Script sürümü. Rapora ve toplu CSV'ye yazılır; böylece eski bir raporun # hangi sürümle üretildiği sonradan anlaşılır. # DEĞİŞTİRİRKEN: labs/envanter/surumler.php dosyasına da aynı sürümü ekleyin. # İkisi uyuşmazsa envanter sayfası uyarı gösterir. $ScriptSurum = '1.3' $ErrorActionPreference = 'SilentlyContinue' $ProgressPreference = 'SilentlyContinue' $script:StartTime = Get-Date # Konsolda Türkçe karakterlerin düzgün görünmesi için try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { } # --------------------------------------------------------------------------- # YARDIMCI FONKSİYONLAR # --------------------------------------------------------------------------- function Get-Safe { param([scriptblock]$Blok, $Varsayilan = $null) try { $r = & $Blok if ($null -eq $r) { return $Varsayilan } return $r } catch { return $Varsayilan } } function Yaz { param([string]$Metin, [string]$Renk = 'Gray') Write-Host $Metin -ForegroundColor $Renk } function Adim { param([string]$Metin) Write-Host (" [*] " + $Metin.PadRight(42)) -NoNewline -ForegroundColor DarkGray } function AdimBitti { param([string]$Sonuc = 'tamam', [string]$Renk = 'DarkGreen') Write-Host $Sonuc -ForegroundColor $Renk } function Boyut { param($Bayt, [int]$Ondalik = 1) if ($null -eq $Bayt -or $Bayt -eq 0) { return '-' } $b = [double]$Bayt if ($b -ge 1TB) { return ('{0} TB' -f [math]::Round($b/1TB, $Ondalik)) } if ($b -ge 1GB) { return ('{0} GB' -f [math]::Round($b/1GB, $Ondalik)) } if ($b -ge 1MB) { return ('{0} MB' -f [math]::Round($b/1MB, $Ondalik)) } return ('{0} KB' -f [math]::Round($b/1KB, $Ondalik)) } function Tarih { param($D) if (-not $D) { return '-' } try { return (Get-Date $D -Format 'dd.MM.yyyy HH\:mm') } catch { return '-' } } function TarihKisa { param($D) if (-not $D) { return '-' } try { return (Get-Date $D -Format 'dd.MM.yyyy') } catch { return '-' } } # UInt16 dizisini metne çevirir (monitör EDID verileri için) function Uint16Metin { param($Dizi) if (-not $Dizi) { return $null } $s = ($Dizi | Where-Object { $_ -gt 0 } | ForEach-Object { [char]$_ }) -join '' return $s.Trim() } <# SMBIOS ALANI GERÇEKTEN DOLU MU? Markalı cihazlarda (Dell, HP, Lenovo) üretici/model/seri no alanları fabrikada doldurulur. Toplama ("noname") kasalarda ise anakart üreticisi bu alanlara şablon değer yazar ve kimse değiştirmez. Bu şablonları gerçek veri sanarsak envanter tablosu kullanılamaz hale gelir. #> $script:SahteDegerler = @( 'to be filled by o.e.m.', 'to be filled by oem', 'default string', 'default', 'system product name', 'system manufacturer', 'system serial number', 'system version', 'system name', 'system model', 'product name', 'manufacturer', 'chassis serial number', 'chassis manufacture', 'base board serial number', 'base board version', 'none', 'n/a', 'na', 'not specified', 'not applicable', 'unknown', 'bilinmiyor', 'invalid', 'oem', 'empty', '0123456789', 'xxxxxxx', 'xxxxxxxx', '123456789', '00000000', 'no asset tag', 'asset-1234567890', 'filled by o.e.m.', 'standard', '.' ) function GecerliMi { param([string]$Deger) if ([string]::IsNullOrWhiteSpace($Deger)) { return $false } $t = $Deger.Trim() if ($t.Length -lt 2) { return $false } # Sadece nokta, tire, alt çizgi veya sıfırdan oluşuyorsa anlamsızdır if ($t -match '^[\.\-_0\s]+$') { return $false } if ($script:SahteDegerler -contains $t.ToLowerInvariant()) { return $false } return $true } # Geçerliyse değeri, değilse $null döndürür function GecerliVeya { param([string]$Deger) if (GecerliMi $Deger) { return $Deger.Trim() } return $null } # --------------------------------------------------------------------------- # ORTAM HAZIRLIĞI # --------------------------------------------------------------------------- Clear-Host Yaz "" Yaz " ============================================================" Cyan Yaz " CİHAZ ENVANTER TOPLAMA" Cyan Yaz " ============================================================" Cyan Yaz "" $Yonetici = Get-Safe { ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } $false if ($Yonetici) { Yaz " Yetki : Yönetici (tam envanter)" DarkGreen } else { Yaz " Yetki : Standart kullanıcı" Yellow Yaz " BitLocker / TPM / disk sağlığı / Secure Boot okunamayacak." DarkYellow Yaz " Tam envanter için PowerShell'i yönetici olarak açın." DarkYellow } if (-not $CiktiKlasoru) { $kok = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } $CiktiKlasoru = Join-Path $kok 'Envanter' } if (-not (Test-Path $CiktiKlasoru)) { New-Item -ItemType Directory -Path $CiktiKlasoru -Force | Out-Null } $Bilgisayar = $env:COMPUTERNAME $DamgaDosya = Get-Date -Format 'yyyyMMdd-HHmm' Yaz " Cihaz : $Bilgisayar" Yaz " Çıktı : $CiktiKlasoru" Yaz "" Yaz " ------------------------------------------------------------" DarkGray $INV = [ordered]@{} $Bulgular = New-Object System.Collections.ArrayList function Bulgu { param( [ValidateSet('Kritik','Uyarı','Bilgi')][string]$Seviye, [string]$Baslik, [string]$Detay, [string]$Oneri = '' ) [void]$Bulgular.Add([pscustomobject]@{ Seviye = $Seviye Baslik = $Baslik Detay = $Detay Oneri = $Oneri }) } <# Ozet16 - kısa parmak izi üretir (SHA256'nın ilk 16 hex hanesi). "Kalıcı Cihaz Kimliği" ile AYNI yöntem, yalnızca öneki farklı. Amaç: MachineGuid ve makine SID'i gibi kimlik değerlerinin ham hâliyle CSV'de ve raporda dolaşmasını önlemek. Özet, iki cihazda karşılaştırılabilir ama geri döndürülüp asıl değeri veremez. Önek özetin neyin özeti olduğunu okunur kılar: KID- (kurulum), SID- (makine). Boş/eksik değer için $null döner; "boşluk özeti" üretip iki cihazı yanlışlıkla eşleştirmez. #> function Ozet16 { param([string]$Deger, [string]$Onek) if ([string]::IsNullOrWhiteSpace($Deger)) { return $null } Get-Safe { $sha = [System.Security.Cryptography.SHA256]::Create() $bayt = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Deger.Trim().ToUpperInvariant())) $sha.Dispose() $Onek + ((([BitConverter]::ToString($bayt)) -replace '-', '').Substring(0, 16)) } $null } # --------------------------------------------------------------------------- # 1. SİSTEM KİMLİĞİ # --------------------------------------------------------------------------- Adim "Sistem kimliği" $cs = Get-Safe { Get-CimInstance Win32_ComputerSystem } $os = Get-Safe { Get-CimInstance Win32_OperatingSystem } $bios = Get-Safe { Get-CimInstance Win32_BIOS } $bb = Get-Safe { Get-CimInstance Win32_BaseBoard } $encl = Get-Safe { Get-CimInstance Win32_SystemEnclosure } $KasaTipleri = @{ 1='Diğer';2='Bilinmiyor';3='Masaüstü';4='İnce Masaüstü';5='Pizza Box';6='Mini Kule'; 7='Kule';8='Taşınabilir';9='Dizüstü';10='Notebook';11='El Cihazı';12='Docking Station'; 13='Hepsi Bir Arada';14='Sub Notebook';15='Alan Tasarruflu';16='Lunch Box';17='Ana Sunucu'; 18='Genişletme Kasası';19='Alt Kasa';20='Veri Yolu Kasası';21='Çevre Birim Kasası'; 22='Depolama Kasası';23='Rack Sunucu';24='Kapalı Masaüstü';30='Tablet';31='Çevrilebilir';32='Ayrılabilir' } $kasaKod = Get-Safe { [int]($encl.ChassisTypes | Select-Object -First 1) } 0 $kasaAdi = if ($KasaTipleri.ContainsKey($kasaKod)) { $KasaTipleri[$kasaKod] } else { "Kod $kasaKod" } # Yalnızca gerçekten taşınabilir kasa kodları. # 18 (Genişletme Kasası) ve 21 (Çevre Birim Kasası) masaüstü kasalarıdır, # buraya dahil edilirse masaüstü cihaz dizüstü sanılır. $Dizustu = $kasaKod -in @(8,9,10,11,12,14,30,31,32) $acilis = Get-Safe { $os.LastBootUpTime } $uptime = if ($acilis) { (Get-Date) - $acilis } else { $null } $uptimeM = if ($uptime) { '{0} gün {1} saat {2} dk' -f $uptime.Days, $uptime.Hours, $uptime.Minutes } else { '-' } $domainMi = Get-Safe { [bool]$cs.PartOfDomain } $false # --- Sanal makine tespiti ------------------------------------------------- $ureticiHam = Get-Safe { "$($cs.Manufacturer)".Trim() } '' $modelHam = Get-Safe { "$($cs.Model)".Trim() } '' $bbUretici = GecerliVeya (Get-Safe { "$($bb.Manufacturer)" } '') $bbModel = GecerliVeya (Get-Safe { "$($bb.Product)" } '') $SanalMi = $false $sanalTip = '' $sanalDesen = @{ 'VMware' = 'VMware' 'VirtualBox' = 'Oracle VirtualBox' 'innotek' = 'Oracle VirtualBox' 'QEMU' = 'QEMU / KVM' 'Xen' = 'Xen' 'Parallels' = 'Parallels' 'Virtual Machine' = 'Hyper-V' 'Microsoft Corporation' = 'Hyper-V' 'Bochs' = 'Bochs' 'KVM' = 'QEMU / KVM' } foreach ($anahtar in $sanalDesen.Keys) { if ("$ureticiHam $modelHam" -match [regex]::Escape($anahtar)) { # "Microsoft Corporation" tek başına Surface cihazlarda da çıkar; # sanal saymak için model de "Virtual Machine" olmalı if ($anahtar -eq 'Microsoft Corporation' -and $modelHam -notmatch 'Virtual') { continue } $SanalMi = $true $sanalTip = $sanalDesen[$anahtar] break } } # --- Cihaz sınıfı --------------------------------------------------------- $CihazSinifi = if ($SanalMi) { "Sanal Makine ($sanalTip)" } elseif ($Dizustu) { 'Dizüstü / Taşınabilir' } elseif ($kasaKod -in @(17,23,25,28)) { 'Sunucu' } else { 'Masaüstü / Kule' } # --- Kimlik alanları: şablon değer varsa anakarta düş --------------------- $ureticiGecerli = GecerliVeya $ureticiHam $modelGecerli = GecerliVeya $modelHam $seriGecerli = GecerliVeya (Get-Safe { "$($bios.SerialNumber)" } '') $bbSeriGecerli = GecerliVeya (Get-Safe { "$($bb.SerialNumber)" } '') $demirbasGecerli = GecerliVeya (Get-Safe { "$($encl.SMBIOSAssetTag)" } '') $KimlikKaynagi = 'SMBIOS (üretici tarafından tanımlı)' $KimlikEksik = $false if (-not $ureticiGecerli) { if ($bbUretici) { $ureticiGecerli = "$bbUretici (anakarttan)"; $KimlikKaynagi = 'Anakart (SMBIOS boş)' } else { $ureticiGecerli = 'Tanımsız (toplama kasa)' ; $KimlikKaynagi = 'Tespit edilemedi' } $KimlikEksik = $true } if (-not $modelGecerli) { if ($bbModel) { $modelGecerli = "$bbModel (anakarttan)" } else { $modelGecerli = 'Tanımsız (toplama kasa)' } $KimlikEksik = $true } if (-not $seriGecerli) { if ($bbSeriGecerli) { $seriGecerli = "$bbSeriGecerli (anakart seri no)" } else { $seriGecerli = 'OEM doldurmamış' } $KimlikEksik = $true } # --- Kalıcı cihaz kimliği ------------------------------------------------- # Aynı fiziksel cihazı ikinci taramada tanıyabilmek için üretilir. # # NEDEN SADECE MachineGuid YETMEZ: # MachineGuid, Windows KURULUMUNA özeldir, cihaza değil. Bir disk klonlanıp # başka cihaza takıldığında ya da iki cihaz aynı imajdan kurulduğunda İKİ # FİZİKSEL CİHAZ AYNI MachineGuid'i taşır ve envanterde tek cihaza çöker. # Sahada bire bir yaşandı: iki HP EliteBook 850 G3, aynı gün aynı imajdan # kurulmuş oldukları için aynı kimliği üretip tek cihaz gibi göründüler. # # Bu yüzden kimlik artık ÖNCE donanım kaynaklarından kurulur (SMBIOS UUID + # seri numaraları). MachineGuid / MAC yalnızca hiçbiri okunamazsa devreye # girer ve bu durum "zayıf kimlik" olarak raporlanır. $smbiosUuid = GecerliVeya (Get-Safe { "$((Get-CimInstance Win32_ComputerSystemProduct -ErrorAction Stop).UUID)" } '') # Bazı OEM'ler UUID alanını hep-sıfır ya da hep-F bırakır; bunlar kimlik değildir. if ($smbiosUuid -and $smbiosUuid -match '^[0\-]+$|^[fF\-]+$') { $smbiosUuid = $null } # Not: $seriGecerli / $bbSeriGecerli yukarıda yedek metinlerle ("OEM doldurmamış", # "... (anakart seri no)") ezilmiş olabilir. Kimlik için ham değerler okunur. $seriBios = GecerliVeya (Get-Safe { "$($bios.SerialNumber)" } '') $seriAnakart = GecerliVeya (Get-Safe { "$($bb.SerialNumber)" } '') $MakineGuid = Get-Safe { (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Cryptography' -Name MachineGuid -ErrorAction Stop).MachineGuid } '' $anaMac = Get-Safe { (Get-CimInstance Win32_NetworkAdapter -Filter 'PhysicalAdapter=TRUE AND MACAddress IS NOT NULL' | Select-Object -First 1).MACAddress } '' $kimlikGirdi = @() $kimlikKaynak = @() $KimlikZayif = $false if ($smbiosUuid) { $kimlikGirdi += "UUID:$smbiosUuid"; $kimlikKaynak += 'SMBIOS UUID' } if ($seriBios) { $kimlikGirdi += "SERI:$seriBios"; $kimlikKaynak += 'BIOS seri no' } if ($seriAnakart) { $kimlikGirdi += "ANAKART:$seriAnakart"; $kimlikKaynak += 'anakart seri no' } if ($kimlikGirdi.Count -eq 0) { $KimlikZayif = $true if ($MakineGuid) { $kimlikGirdi += "MGUID:$MakineGuid"; $kimlikKaynak += 'MachineGuid (klonlanmış kurulumlarda çakışır)' } elseif ($anaMac) { $kimlikGirdi += "MAC:$anaMac"; $kimlikKaynak += 'MAC adresi (ağ kartı değişirse bozulur)' } else { $kimlikGirdi += "AD:$Bilgisayar"; $kimlikKaynak += 'bilgisayar adı (en zayıf kaynak)' } } # Girdiler tek bir parmak izine indirgenir: CSV'de kısa durur, seri numarasını # düz metin olarak ikinci kez taşımaz, kaynak sırası değişse de sonuç aynıdır. $kimlikHam = ($kimlikGirdi -join '|').ToUpperInvariant() $CihazKimligi = Get-Safe { $sha = [System.Security.Cryptography.SHA256]::Create() $bayt = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($kimlikHam)) $sha.Dispose() 'CID-' + ((([BitConverter]::ToString($bayt)) -replace '-', '').Substring(0, 16)) } $kimlikHam $KimlikKaynagiMetin = ($kimlikKaynak -join ' + ') $INV['Sistem'] = [pscustomobject]@{ 'Bilgisayar Adı' = $Bilgisayar 'Cihaz Sınıfı' = $CihazSinifi 'Üretici' = $ureticiGecerli 'Model' = $modelGecerli 'Seri No / Servis Etiketi' = $seriGecerli 'Marka / Model Kaynağı' = $KimlikKaynagi 'Kimlik Kaynağı' = $KimlikKaynagiMetin 'Kalıcı Cihaz Kimliği' = $CihazKimligi 'Demirbaş Etiketi' = if ($demirbasGecerli) { $demirbasGecerli } else { 'Tanımsız' } 'Kasa Tipi' = "$kasaAdi$(if($Dizustu){' (taşınabilir)'}else{''})" 'Anakart' = $(if ("$bbUretici$bbModel".Trim()) { "$bbUretici $bbModel".Trim() } else { 'Tanımsız' }) 'Anakart Seri No' = if ($bbSeriGecerli) { $bbSeriGecerli } else { 'Tanımsız' } 'BIOS Sürümü' = Get-Safe { "$($bios.SMBIOSBIOSVersion) ($(TarihKisa $bios.ReleaseDate))" } '-' 'Etki Alanı / Çalışma Grubu' = if ($domainMi) { "$($cs.Domain) [ETKİ ALANI]" } else { "$($cs.Workgroup) [Çalışma Grubu]" } 'Oturum Açan Kullanıcı' = Get-Safe { $cs.UserName } '-' 'Son Açılış' = Tarih $acilis 'Çalışma Süresi' = $uptimeM 'Sistem Tipi' = Get-Safe { $cs.SystemType } '-' 'Envanter Tarihi' = Get-Date -Format 'dd.MM.yyyy HH\:mm' 'Envanter Aracı Sürümü' = $ScriptSurum } if ($KimlikEksik -and -not $SanalMi) { Bulgu -Seviye 'Bilgi' -Baslik 'Cihaz kimliği SMBIOS''ta tanımlı değil' ` -Detay 'Üretici/model/seri no alanları fabrikada doldurulmamış. Toplama (montaj) kasalarda olağandır; cihaz seri numarasıyla takip edilemez.' ` -Oneri 'Kasaya fiziksel demirbaş etiketi yapıştırılıp envanterde "Kalıcı Cihaz Kimliği" ile eşleştirilmeli.' } if ($KimlikZayif -and -not $SanalMi) { Bulgu -Seviye 'Uyarı' -Baslik 'Cihaz kimliği zayıf kaynaktan üretildi' ` -Detay "Donanım kimliği (SMBIOS UUID, BIOS/anakart seri no) okunamadı. Kimlik şu kaynaktan üretildi: $KimlikKaynagiMetin. Bu kaynak cihaza değil kuruluma ya da ağ kartına bağlıdır; klonlanmış kurulumlarda iki farklı cihaz aynı kimliği alabilir." ` -Oneri 'Bu cihaz envanterde demirbaş etiketiyle takip edilmeli; toplu CSV''de kimlik sütununa güvenilmemeli.' } # --- KURULUM PARMAK İZLERİ (klon/imaj kontrolü için) ---------------------- # # Buradaki değerler CİHAZI değil, ÜZERİNDEKİ WINDOWS KURULUMUNU tanımlar. # "Kalıcı Cihaz Kimliği" donanımdan üretiliyor; bu ikisi birlikte, iki # cihazın aynı kurulumdan çoğaltılıp çoğaltılmadığını söyler: # donanım farklı + kurulum aynı -> klon / aynı imaj # # BİRİNCİL sinyaller (sysprep bunları SIFIRLAR, bu yüzden ayırt edicidirler): # - Kurulum Kimliği : MachineGuid özeti (yukarıda zaten okundu, tekrar okunmuyor) # - Makine SID : cihazın kendi hesap alanı kimliği # DESTEKLEYİCİ sinyaller (sysprep bunları sıfırlamaz, tek başlarına kanıt DEĞİL): # - C: birim seri no, Windows kurulum damgası $KurulumKimligi = Ozet16 $MakineGuid 'KID-' <# MAKİNE SID'İ - buradaki tuzak ölümcül olduğu için ayrıntılı açıklanıyor: ProfileList altında yalnızca YEREL hesaplar değil, cihaza oturum açmış ETKİ ALANI kullanıcılarının SID'leri de durur. Etki alanı SID'inin alan kısmı o etki alanındaki BÜTÜN cihazlarda aynıdır. Ayrım yapılmadan okunursa aynı etki alanındaki her cihaz birbirinin klonu ilan edilir - yani özellik en kalabalık ortamda tamamen yanlış çalışır. Bu yüzden: - Yerleşik Yönetici hesabının SID'i (-500) tartışmasız makinenin kendi SID'idir; varsa doğrudan o kullanılır. - -500 profili yoksa (çoğu cihazda o hesapla hiç oturum açılmaz) yalnızca ETKİ ALANINA ÜYE OLMAYAN cihazlarda -1000 ve üzeri yerel profillere düşülür; çalışma grubunda etki alanı SID'i bulunamayacağı için güvenlidir. - Etki alanına üye cihazda -500 yoksa değer ÜRETİLMEZ ($null). Eksik sinyal, yanlış sinyalden iyidir: karar mantığı tek birincille en fazla "şüpheli" der, "klon" diyemez. #> $MakineSidHam = Get-Safe { $profilYolu = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' $sidler = @(Get-ChildItem $profilYolu -ErrorAction Stop | ForEach-Object { $_.PSChildName } | Where-Object { $_ -match '^S-1-5-21-\d+-\d+-\d+-\d+$' }) $bulunan = $null foreach ($s in $sidler) { if (([int]($s -split '-')[-1]) -eq 500) { $bulunan = $s; break } } if (-not $bulunan -and -not $domainMi) { $bulunan = $sidler | Where-Object { ([int]($_ -split '-')[-1]) -ge 1000 } | Sort-Object { [int]($_ -split '-')[-1] } | Select-Object -First 1 } # Son parça (RID) atılır; geriye makinenin hesap alanı kimliği kalır. if ($bulunan) { $bulunan -replace '-\d+$', '' } else { $null } } $null $MakineSidOzeti = Ozet16 $MakineSidHam 'SID-' # Destekleyici 1: sistem sürücüsünün birim seri numarası. # Ham disk kopyasında birebir korunur; yeniden biçimlendirmede değişir. $CBirimSeri = Get-Safe { $sd = $env:SystemDrive "$((Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$sd'" -ErrorAction Stop).VolumeSerialNumber)" } '' $CBirimSeri = GecerliVeya $CBirimSeri # Destekleyici 2: Windows kurulum anı, saniye hassasiyetinde. # Registry'deki InstallTime KULLANILMIYOR: Windows 7'de yok ve okunamadığında # 1601-01-01 üretip her cihazda aynı çıkarak sahte eşleşme yaratıyor. $KurulumDamgasi = Get-Safe { (Get-Date $os.InstallDate -Format 'yyyy-MM-dd HH:mm:ss') } '' AdimBitti # --------------------------------------------------------------------------- # 2. İŞLETİM SİSTEMİ + LİSANS # --------------------------------------------------------------------------- Adim "İşletim sistemi ve lisans" $cv = Get-Safe { Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' } $displayV = Get-Safe { $cv.DisplayVersion } (Get-Safe { $cv.ReleaseId } '-') $ubr = Get-Safe { $cv.UBR } 0 $build = Get-Safe { "$($cv.CurrentBuild).$ubr" } '-' $lisans = Get-Safe { Get-CimInstance SoftwareLicensingProduct -Filter "ApplicationID='55c92734-d682-4d71-983e-d6ec3f16059f' AND PartialProductKey IS NOT NULL" | Select-Object -First 1 } $lisansKod = Get-Safe { [int]$lisans.LicenseStatus } if ($null -eq $lisansKod) { $lisansKod = -1 } $LisansDurumu = switch ($lisansKod) { 0 { 'Lisanssız' } 1 { 'Etkinleştirilmiş' } 2 { 'OOB Deneme' } 3 { 'OOT Deneme' } 4 { 'Ek Süre (Grace)' } 5 { 'Bildirim (etkin değil)' } 6 { 'Ek Süre Uzatıldı' } default { 'Belirlenemedi' } } $LisansKanali = Get-Safe { $lisans.ProductKeyChannel } '-' $oemKey = Get-Safe { (Get-CimInstance SoftwareLicensingService).OA3xOriginalProductKey } '' $osAdi = Get-Safe { $os.Caption.Trim() } '-' if ((Get-Safe { [int]$cv.CurrentBuild } 0) -ge 22000 -and $osAdi -match 'Windows 10') { $osAdi = $osAdi -replace 'Windows 10','Windows 11' } $INV['İşletim Sistemi'] = [pscustomobject]@{ 'İşletim Sistemi' = $osAdi 'Sürüm' = $displayV 'Yapı (Build)' = $build 'Mimari' = Get-Safe { $os.OSArchitecture } '-' 'Kurulum Tarihi' = Tarih (Get-Safe { $os.InstallDate }) 'Windows Dizini' = Get-Safe { $os.WindowsDirectory } '-' 'Arayüz Dili' = Get-Safe { "$((Get-UICulture).Name) - $((Get-UICulture).DisplayName)" } '-' 'Biçim / Bölge' = Get-Safe { "$((Get-Culture).Name) - $((Get-Culture).DisplayName)" } '-' 'Saat Dilimi' = Get-Safe { (Get-TimeZone).DisplayName } (Get-Safe { (Get-CimInstance Win32_TimeZone).Caption } '-') 'Lisans Durumu' = $LisansDurumu 'Lisans Kanalı' = $LisansKanali 'OEM Anahtarı (BIOS)' = if ($oemKey) { $oemKey } else { 'Yok (dijital/hacim lisansı)' } 'PowerShell Sürümü' = $PSVersionTable.PSVersion.ToString() } if ($LisansDurumu -ne 'Etkinleştirilmiş') { Bulgu -Seviye 'Kritik' -Baslik 'Windows etkinleştirilmemiş' ` -Detay "Lisans durumu: $LisansDurumu" ` -Oneri 'Lisans temin edilip etkinleştirme yapılmalı.' } <# İŞLETİM SİSTEMİ DESTEK DURUMU Tespit YAPI NUMARASINDAN yapılır, $os.Caption metninden değil. Caption bazı dillerde çevrilir; yapı numarası ise her dilde aynıdır. ProductType de numerik (1=iş istasyonu, 2=etki alanı denetleyicisi, 3=sunucu) ve Windows 7 / Server 2008 R2 gibi aynı yapıyı paylaşan sürümleri ayırır. #> $osBuildNo = Get-Safe { [int]$cv.CurrentBuild } 0 $urunTipi = Get-Safe { [int]$os.ProductType } 1 $SunucuMu = $urunTipi -ne 1 $eolBilgi = $null if ($SunucuMu) { if ($osBuildNo -le 3790) { $eolBilgi = @{ Ad='Windows Server 2003'; Tarih='14.07.2015' } } elseif ($osBuildNo -le 6002) { $eolBilgi = @{ Ad='Windows Server 2008'; Tarih='14.01.2020' } } elseif ($osBuildNo -le 7601) { $eolBilgi = @{ Ad='Windows Server 2008 R2'; Tarih='14.01.2020' } } elseif ($osBuildNo -le 9200) { $eolBilgi = @{ Ad='Windows Server 2012'; Tarih='10.10.2023' } } elseif ($osBuildNo -le 9600) { $eolBilgi = @{ Ad='Windows Server 2012 R2'; Tarih='10.10.2023' } } # Server 2016 (14393) / 2019 (17763) / 2022 (20348) hâlâ destekli } else { if ($osBuildNo -le 2600) { $eolBilgi = @{ Ad='Windows XP'; Tarih='08.04.2014' } } elseif ($osBuildNo -le 6002) { $eolBilgi = @{ Ad='Windows Vista'; Tarih='11.04.2017' } } elseif ($osBuildNo -le 7601) { $eolBilgi = @{ Ad='Windows 7'; Tarih='14.01.2020' } } elseif ($osBuildNo -le 9200) { $eolBilgi = @{ Ad='Windows 8'; Tarih='12.01.2016' } } elseif ($osBuildNo -le 9600) { $eolBilgi = @{ Ad='Windows 8.1'; Tarih='10.01.2023' } } elseif ($osBuildNo -lt 22000) { $eolBilgi = @{ Ad='Windows 10'; Tarih='14.10.2025' } } } $DestekDurumu = if ($eolBilgi) { "Sona erdi ($($eolBilgi.Tarih))" } else { 'Destekleniyor' } $INV['İşletim Sistemi'] | Add-Member -NotePropertyName 'Destek Durumu' -NotePropertyValue $DestekDurumu -Force if ($eolBilgi) { Bulgu -Seviye 'Kritik' -Baslik 'İşletim sistemi desteği sona erdi' ` -Detay "$($eolBilgi.Ad) (yapı $osBuildNo) için Microsoft desteği $($eolBilgi.Tarih) tarihinde bitti. Cihaz artık güvenlik yaması almıyor." ` -Oneri 'Donanım destekliyorsa Windows 11''e yükseltilmeli; desteklemiyorsa cihaz yenileme planına alınmalı. (LTSC/IoT sürümlerinde destek süresi daha uzun olabilir, sürüm doğrulanmalı.)' } AdimBitti # --------------------------------------------------------------------------- # 3. İŞLEMCİ # --------------------------------------------------------------------------- Adim "İşlemci" $cpuList = @(Get-Safe { Get-CimInstance Win32_Processor } @()) $INV['İşlemci'] = @( foreach ($c in $cpuList) { [pscustomobject]@{ 'Model' = Get-Safe { $c.Name.Trim() } '-' 'Üretici' = Get-Safe { $c.Manufacturer } '-' 'Çekirdek' = Get-Safe { $c.NumberOfCores } '-' 'Mantıksal' = Get-Safe { $c.NumberOfLogicalProcessors } '-' 'Temel Hız' = Get-Safe { "$([math]::Round($c.MaxClockSpeed/1000,2)) GHz" } '-' 'Soket' = Get-Safe { $c.SocketDesignation } '-' 'L2 / L3 Önbellek' = Get-Safe { "$([math]::Round($c.L2CacheSize/1024,1)) MB / $([math]::Round($c.L3CacheSize/1024,1)) MB" } '-' 'Sanallaştırma' = Get-Safe { if ($c.VirtualizationFirmwareEnabled) { 'Açık' } else { 'Kapalı / Desteklenmiyor' } } '-' } } ) AdimBitti # --------------------------------------------------------------------------- # 4. BELLEK (RAM) - SLOT DETAYLI # --------------------------------------------------------------------------- Adim "Bellek (RAM) ve slotlar" $BellekTipi = @{ 0='Bilinmiyor';1='Diğer';2='DRAM';20='DDR';21='DDR2';22='DDR2 FB-DIMM'; 24='DDR3';26='DDR4';27='LPDDR';28='LPDDR2';29='LPDDR3';30='LPDDR4'; 34='DDR5';35='LPDDR5' } $FormFaktor = @{ 8='DIMM'; 12='SODIMM'; 13='SRIMM'; 0='Bilinmiyor' } $ramList = @(Get-Safe { Get-CimInstance Win32_PhysicalMemory } @()) $ramArray = Get-Safe { Get-CimInstance Win32_PhysicalMemoryArray | Select-Object -First 1 } $toplamSlot = Get-Safe { [int]$ramArray.MemoryDevices } $ramList.Count $doluSlot = $ramList.Count $toplamRam = Get-Safe { ($ramList | Measure-Object -Property Capacity -Sum).Sum } 0 if (-not $toplamRam) { $toplamRam = Get-Safe { $cs.TotalPhysicalMemory } 0 } # ECC bellek - iş istasyonu ve sunucularda anlamlı $eccKod = Get-Safe { [int]$ramArray.MemoryErrorCorrection } 0 $eccMetin = switch ($eccKod) { 1 { 'Diğer' } 2 { 'Bilinmiyor' } 3 { 'Yok (standart bellek)' } 4 { 'Parity' } 5 { 'ECC (tek bit düzeltme)' } 6 { 'ECC (çok bit düzeltme)' } 7 { 'CRC' } default { 'Bilinmiyor' } } $INV['Bellek Özeti'] = [pscustomobject]@{ 'Toplam RAM' = Boyut $toplamRam 'Slot Kullanımı' = "$doluSlot / $toplamSlot dolu" 'Boş Slot' = [math]::Max(0, $toplamSlot - $doluSlot) 'Maks. Desteklenen' = Get-Safe { Boyut ([double]$ramArray.MaxCapacityEx * 1024) } '-' 'Hata Düzeltme' = $eccMetin 'Kullanılabilir' = Get-Safe { Boyut ([double]$os.FreePhysicalMemory * 1024) } '-' 'Kullanım' = Get-Safe { $t = [double]$os.TotalVisibleMemorySize; $f = [double]$os.FreePhysicalMemory "%{0}" -f [math]::Round((($t - $f) / $t) * 100, 0) } '-' } $INV['Bellek Modülleri'] = @( foreach ($m in $ramList) { $tk = Get-Safe { [int]$m.SMBIOSMemoryType } 0 if ($tk -eq 0) { $tk = Get-Safe { [int]$m.MemoryType } 0 } $ff = Get-Safe { [int]$m.FormFactor } 0 [pscustomobject]@{ 'Slot' = Get-Safe { $m.DeviceLocator } '-' 'Kapasite' = Boyut (Get-Safe { $m.Capacity } 0) 'Tip' = if ($BellekTipi.ContainsKey($tk)) { $BellekTipi[$tk] } else { "Kod $tk" } 'Form' = if ($FormFaktor.ContainsKey($ff)) { $FormFaktor[$ff] } else { "Kod $ff" } 'Hız' = Get-Safe { "$($m.Speed) MHz" } '-' 'Üretici' = Get-Safe { $m.Manufacturer.Trim() } '-' 'Parça No' = Get-Safe { $m.PartNumber.Trim() } '-' 'Seri No' = Get-Safe { $m.SerialNumber.Trim() } '-' } } ) $ramGB = [math]::Round($toplamRam / 1GB, 0) if ($ramGB -gt 0 -and $ramGB -lt 8) { Bulgu -Seviye 'Uyarı' -Baslik 'Düşük RAM kapasitesi' ` -Detay "Toplam $ramGB GB RAM. Günümüz ofis kullanımı için en az 8 GB önerilir." ` -Oneri "$([math]::Max(0,$toplamSlot-$doluSlot)) boş slot mevcut, yükseltme yapılabilir." } AdimBitti # --------------------------------------------------------------------------- # 5. DEPOLAMA - HARİCİ DİSK AYRIMI # --------------------------------------------------------------------------- Adim "Depolama düzeni" <# NEDEN BU BÖLÜM VAR: Bu araç sahada genellikle bir USB bellekten çalıştırılıyor. Windows takılı belleği çoğu zaman sıradan bir disk gibi gösterir - özellikle harici SSD'ler ve bazı USB bellekler kendilerini "yerel disk" (DriveType=3) olarak tanıtır. Sonuç: aracın KENDİ USB'si, gezilen HER cihazın envanterine o cihazın donanımıymış gibi giriyordu. Fiziksel disk tablosuna, sürücü listesine, toplu CSV'nin Disk sütununa, hatta doluluk ve BitLocker bulgularına. Yani tek bir yanlış disk, kırk cihazın kaydını birden kirletiyordu ve müşteriye giden tabloda o cihazda olmayan bir disk görünüyordu. NASIL AYIRT EDİLİYOR: Ayrım Win32_DiskDrive üzerinden yapılıyor; bu sınıf Windows 7 dahil her sürümde vardır, Get-PhysicalDisk ise yoktur. Bir disk şu işaretlerden herhangi birini taşıyorsa harici sayılıyor: arayüzü USB ya da 1394, ortam tipi Removable/External, veya aygıt kimliği USBSTOR altında. GÜVENLİK AĞI - SİSTEM DİSKİ ASLA HARİÇ TUTULMAZ: Cihazın kendi diskini yanlışlıkla gizlemek, fazladan bir disk göstermekten çok daha kötü bir hatadır: envanterde diski olmayan bir bilgisayar kalır. Bu yüzden Windows'un kurulu olduğu bölümü taşıyan fiziksel disk, yukarıdaki işaretlerin hepsine uysa bile dahili kabul edilir. Bu kural aynı zamanda aracın C:'den (İndirilenler klasöründen ya da tek satırlık komutla geçici klasörden) çalıştırıldığı durumu da kendiliğinden doğru ele alır. BİLGİ SİLİNMİYOR, AYRILIYOR: Harici diskler rapordan atılmıyor; "Harici / Çıkarılabilir Depolama" bölümüne taşınıyor ve aracın çalıştığı bellek ayrıca işaretleniyor. Cihaza takılı bir yedekleme diski bilinmeye değer bir bilgidir, ama o cihazın dahili donanımı değildir. #> $diskSurucular = @(Get-Safe { Get-CimInstance Win32_DiskDrive } @()) $DiskHarici = @{} # disk numarası -> $true/$false $SurucuDisk = @{} # 'E:' -> disk numarası foreach ($dd in $diskSurucular) { $ix = Get-Safe { [int]$dd.Index } (-1) if ($ix -lt 0) { continue } $arayuz = "$(Get-Safe { $dd.InterfaceType } '')" $ortam = "$(Get-Safe { $dd.MediaType } '')" $pnp = "$(Get-Safe { $dd.PNPDeviceID } '')" $DiskHarici[$ix] = ($arayuz -in @('USB','1394')) -or ($ortam -match 'Removable|External') -or ($pnp -match 'USBSTOR') # Sürücü harfi eşlemesi: disk -> bölüm -> mantıksal sürücü. Harici bir # diskin hangi harfle göründüğünü ancak bu zincirle bilebiliyoruz; # DriveType tek başına yeterli değil (harici SSD'ler 3 döndürüyor). foreach ($bol in (Get-Safe { Get-CimAssociatedInstance -InputObject $dd -ResultClassName Win32_DiskPartition } @())) { foreach ($mnt in (Get-Safe { Get-CimAssociatedInstance -InputObject $bol -ResultClassName Win32_LogicalDisk } @())) { $harf = "$(Get-Safe { $mnt.DeviceID } '')" if ($harf) { $SurucuDisk[$harf] = $ix } } } } # Sistem diskinin numarası. Hem güvenlik ağı hem de aşağıdaki SD/MMC kuralı # buna dayanıyor, o yüzden zenginleştirmeden ÖNCE hesaplanıyor. $SistemDiskNo = $null if ($SurucuDisk.ContainsKey("$env:SystemDrive")) { $SistemDiskNo = $SurucuDisk["$env:SystemDrive"] } <# İKİNCİ KAYNAK - Get-PhysicalDisk.BusType: USB köprüsü kullanan bazı harici SSD'ler Win32_DiskDrive tarafında SATA/SCSI arayüzü ve "sabit disk" ortamı olarak görünür; gerçeği yalnızca BusType söyler. İki kaynak burada, tek bir tabloda birleştiriliyor. Birleştirilmezse disk "Harici Depolama" bölümüne taşınırken sürücü harfi "Sürücüler" tablosunda kalır ve o harf için doluluk / BitLocker bulguları üretilmeye devam ederdi - yani sorunun yarısı çözülmüş olurdu. Bu çağrı burada yapılıp aşağıdaki fiziksel disk bölümünde tekrar kullanılıyor; Get-PhysicalDisk yavaş bir çağrıdır, iki kez sorulmuyor. #> # 4=1394 ve 7=USB her koşulda harici; Windows bu yollardan kurulmaz. # 12=SD ve 13=MMC ise ANCAK sistem diskini kesin bulabildiysek harici # sayılıyor: ucuz tablet ve netbooklarda Windows'un kendisi eMMC üzerindedir, # eşleme kurulamamışsa o cihaz envantere disksiz geçerdi. $CikarilabilirYol = if ($null -ne $SistemDiskNo) { @(4, 7, 12, 13) } else { @(4, 7) } $fizDiskler = @(Get-Safe { Get-PhysicalDisk } @()) foreach ($pd in $fizDiskler) { $pno = if ($null -ne $pd.DeviceId) { Get-Safe { [int]$pd.DeviceId } $null } else { $null } if ($null -eq $pno) { continue } $pbt = Get-Safe { [int]$pd.BusType } 0 if ($pbt -in $CikarilabilirYol) { $DiskHarici[$pno] = $true } } # GÜVENLİK AĞI: sistem diskini taşıyan fiziksel disk her koşulda dahilidir. # Yukarıdaki bütün kuralları ezmesi için EN SONA konuldu. if ($null -ne $SistemDiskNo) { $DiskHarici[$SistemDiskNo] = $false } # Aracın kendisi hangi sürücüden çalışıyor? Rapordaki işaret için kullanılıyor; # harici sayma kararını TEK BAŞINA vermez - C:'den çalıştırılmış olabilir. $KendiSurucu = Get-Safe { (Split-Path -Qualifier $PSScriptRoot).ToUpper() } '' $KendiDiskNo = if ($KendiSurucu -and $SurucuDisk.ContainsKey($KendiSurucu)) { $SurucuDisk[$KendiSurucu] } else { $null } <# Bir disk numarası harici mi? Bilinmeyen numara dahili sayılır - bilgi yokken diski gizlemek yanlış tarafa düşmek olur. #> function DiskHariciMi { param($DiskNo) if ($null -eq $DiskNo) { return $false } if ($DiskNo -eq $script:SistemDiskNo) { return $false } return [bool]($script:DiskHarici[[int]$DiskNo]) } <# Bir sürücü harfi ('E:') hangi disk numarasında duruyor? Eşleşme yoksa $null - çağıran taraf bunu "bilinmiyor" olarak ele alıyor. #> function SurucununDiski { param([string]$Harf) if (-not $Harf) { return $null } if (-not $script:SurucuDisk.ContainsKey($Harf)) { return $null } return $script:SurucuDisk[$Harf] } <# Get-PhysicalDisk kaydının disk numarası. DeviceId hiç yoksa $null döner - [int]$null sessizce 0 verip kaydı disk 0'a yamayacağı için ayrıca bakılıyor. #> function FizikselDiskNo { param($Disk) if ($null -eq $Disk.DeviceId) { return $null } return (Get-Safe { [int]$Disk.DeviceId } $null) } <# Get-PhysicalDisk kaydı harici mi? Her iki kaynak da yukarıda $DiskHarici tablosunda birleştirildiği için burada tek bir yere bakmak yeterli - fiziksel disk, sürücü harfi ve bulgu tarafı aynı kararı görüyor. #> function FizikselHariciMi { param($Disk) return (DiskHariciMi (FizikselDiskNo $Disk)) } <# Bir disk numarasına hangi sürücü harfleri düşüyor? Harici depolama tablosunda "bu hangi harf" sorusunu cevaplamak için. #> function DiskinSuruculeri { param($DiskNo) if ($null -eq $DiskNo) { return '-' } $h = @($script:SurucuDisk.Keys | Where-Object { $script:SurucuDisk[$_] -eq $DiskNo } | Sort-Object) if ($h.Count -eq 0) { return '-' } return ($h -join ', ') } <# Harici depolama satırındaki açıklama. Aracın kendi belleğini ayrıca işaretlemek önemli: raporu okuyan kişi o satırın müşterinin donanımı olmadığını tereddütsüz görmeli. #> function HariciNot { param($DiskNo) if ($null -ne $DiskNo -and $null -ne $script:KendiDiskNo -and $DiskNo -eq $script:KendiDiskNo) { return 'Envanter aracının çalıştığı bellek - cihaza ait değil' } return 'Cihaza takılı harici depolama' } $HariciDepolama = New-Object System.Collections.ArrayList AdimBitti # --------------------------------------------------------------------------- # 5b. DEPOLAMA - FİZİKSEL DİSKLER # --------------------------------------------------------------------------- Adim "Fiziksel diskler" $MedyaTipi = @{ 0='Bilinmiyor'; 3='HDD'; 4='SSD'; 5='SCM' } $VeriYolu = @{ 0='Bilinmiyor';1='SCSI';2='ATAPI';3='ATA';4='1394';5='SSA';6='Fibre';7='USB';8='RAID';9='iSCSI';10='SAS';11='SATA';12='SD';13='MMC';17='NVMe' } # $fizDiskler yukarıdaki depolama düzeni bölümünde bir kez okundu. if ($fizDiskler.Count -gt 0) { $INV['Fiziksel Diskler'] = @( foreach ($d in $fizDiskler) { $mt = Get-Safe { [int]$d.MediaType } 0 $bt = Get-Safe { [int]$d.BusType } 0 # Get-PhysicalDisk.DeviceId ile Win32_DiskDrive.Index aynı numaradır; # iki kaynak bu numara üzerinden birleşiyor. $diskNo = FizikselDiskNo $d if (FizikselHariciMi $d) { [void]$HariciDepolama.Add([pscustomobject]@{ 'Model' = Get-Safe { $d.FriendlyName } '-' 'Sürücü' = DiskinSuruculeri $diskNo 'Kapasite' = Boyut (Get-Safe { $d.Size } 0) 'Bağlantı' = if ($VeriYolu.ContainsKey($bt)) { $VeriYolu[$bt] } else { "Kod $bt" } 'Seri No' = Get-Safe { $d.SerialNumber.Trim() } '-' 'Not' = HariciNot $diskNo }) continue } $rel = if ($Yonetici) { Get-Safe { $d | Get-StorageReliabilityCounter } } else { $null } $tipAdi = if ($MedyaTipi.ContainsKey($mt)) { $MedyaTipi[$mt] } else { "$($d.MediaType)" } if ($tipAdi -in @('Bilinmiyor','0') -and $bt -eq 17) { $tipAdi = 'SSD (NVMe)' } # Saglik once NUMERIK degerden okunur (0=Healthy 1=Warning 2=Unhealthy); # numerik cevrilemiyorsa metne dusulur. Boylece dilden bagimsiz olur. $saglikHam = "$($d.HealthStatus)" $saglikKod = Get-Safe { [int]$d.HealthStatus } $null $saglikTR = if ($null -ne $saglikKod) { switch ($saglikKod) { 0 { 'Sağlıklı' } 1 { 'UYARI' } 2 { 'SORUNLU' } default { if ($saglikHam) { $saglikHam } else { '-' } } } } else { switch ($saglikHam) { 'Healthy' { 'Sağlıklı' } 'Warning' { 'UYARI' } 'Unhealthy' { 'SORUNLU' } default { if ($saglikHam) { $saglikHam } else { '-' } } } } [pscustomobject]@{ 'Model' = Get-Safe { $d.FriendlyName } '-' 'Seri No' = Get-Safe { $d.SerialNumber.Trim() } '-' 'Kapasite' = Boyut (Get-Safe { $d.Size } 0) 'Tip' = $tipAdi 'Bağlantı' = if ($VeriYolu.ContainsKey($bt)) { $VeriYolu[$bt] } else { "Kod $bt" } 'Sağlık' = $saglikTR 'Çalışma Saati' = if ($rel) { Get-Safe { "$($rel.PowerOnHours) saat (~$([math]::Round($rel.PowerOnHours/8760,1)) yıl)" } '-' } else { if ($Yonetici) { '-' } else { 'yetki gerekli' } } 'Aşınma (SSD)' = if ($rel -and $null -ne $rel.Wear) { "%$($rel.Wear)" } else { '-' } 'Sıcaklık' = if ($rel -and $rel.Temperature) { "$($rel.Temperature) C" } else { '-' } 'Ürün Yazılımı' = Get-Safe { $d.FirmwareVersion } '-' } } ) foreach ($d in $fizDiskler) { # Harici diskler bulgu üretmez. Aksi halde aracın kendi USB belleği, # gezilen her cihazda aynı "disk sağlık uyarısı" bulgusunu düşürürdü. if (FizikselHariciMi $d) { continue } $kod = Get-Safe { [int]$d.HealthStatus } $null $sorunlu = if ($null -ne $kod) { $kod -notin @(0, 5) } # 0=Healthy, 5=Unknown else { "$($d.HealthStatus)" -notin @('Healthy','Unknown','') } if ($sorunlu) { Bulgu -Seviye 'Kritik' -Baslik 'Disk sağlık uyarısı' ` -Detay "$($d.FriendlyName) diski '$($d.HealthStatus)' durumunda." ` -Oneri 'Veriler acilen yedeklenmeli, disk değişimi planlanmalı.' } } } else { # Windows 7 / Get-PhysicalDisk bulunmayan sistemler. Harici ayrımı burada # da geçerli; zaten sınıflandırma bu sınıftan (Win32_DiskDrive) üretiliyor. $INV['Fiziksel Diskler'] = @( foreach ($d in $diskSurucular) { $diskNo = Get-Safe { [int]$d.Index } $null if (DiskHariciMi $diskNo) { [void]$HariciDepolama.Add([pscustomobject]@{ 'Model' = Get-Safe { $d.Model } '-' 'Sürücü' = DiskinSuruculeri $diskNo 'Kapasite' = Boyut (Get-Safe { $d.Size } 0) 'Bağlantı' = Get-Safe { $d.InterfaceType } '-' 'Seri No' = Get-Safe { $d.SerialNumber.Trim() } '-' 'Not' = HariciNot $diskNo }) continue } [pscustomobject]@{ 'Model' = Get-Safe { $d.Model } '-' 'Seri No' = Get-Safe { $d.SerialNumber.Trim() } '-' 'Kapasite' = Boyut (Get-Safe { $d.Size } 0) 'Tip' = Get-Safe { if ($d.Model -match 'SSD|NVMe|Solid') { 'SSD' } else { 'HDD/Bilinmiyor' } } '-' 'Bağlantı' = Get-Safe { $d.InterfaceType } '-' 'Bölüm' = Get-Safe { $d.Partitions } '-' 'Durum' = Get-Safe { $d.Status } '-' } } ) } AdimBitti # --------------------------------------------------------------------------- # 6. DEPOLAMA - MANTIKSAL SÜRÜCÜLER + BITLOCKER # --------------------------------------------------------------------------- Adim "Sürücüler ve şifreleme" <# BitLocker YETENEK kontrolü - neden ayrı bakılıyor: Windows 7'de ve BitLocker özelliği bulunmayan sürümlerde (Home vb.) Get-BitLockerVolume komutu HİÇ YOKTUR. Eskiden bu durumda da sürücüye "KAPALI" yazılıyor ve şifreleme bulgusu üretiliyordu; yani araç bakamadığı bir şeyi "kapalı" diye raporluyordu. Bu yanlış bir Kritik bulguydu. Artık komutun varlığı ayrıca sınanıyor: yetenek yoksa "Belirlenemedi" yazılır ve bulgu ÜRETİLMEZ. #> $BitLockerYetenek = $null -ne (Get-Safe { Get-Command Get-BitLockerVolume -ErrorAction Stop }) $bitlocker = @{} if ($Yonetici -and $BitLockerYetenek) { foreach ($bv in (Get-Safe { Get-BitLockerVolume } @())) { if ($bv.MountPoint) { $bitlocker[$bv.MountPoint] = $bv } } } # DriveType=3 "yerel disk" demektir ama harici SSD'ler ve bazı USB bellekler # de kendilerini böyle tanıtır. Bu listeye düşen harici sürücüler aşağıda # ayıklanıp 'Diğer Sürücüler' bölümüne aktarılıyor - yoksa doluluk ve # BitLocker bulguları aracın kendi belleği için üretiliyordu. $hariciMantiksal = New-Object System.Collections.ArrayList $INV['Sürücüler'] = @( foreach ($v in (Get-Safe { Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' } @())) { $harf = "$(Get-Safe { $v.DeviceID } '')" $harfNo = SurucununDiski $harf if (DiskHariciMi $harfNo) { [void]$hariciMantiksal.Add([pscustomobject]@{ 'Sürücü' = $harf 'Tip' = 'Harici Disk' 'Etiket' = Get-Safe { $v.VolumeName } '-' 'Hedef' = HariciNot $harfNo 'Toplam' = Boyut $v.Size }) continue } $doluOran = if ($v.Size -gt 0) { [math]::Round((($v.Size - $v.FreeSpace) / $v.Size) * 100, 0) } else { 0 } $bl = $bitlocker["$($v.DeviceID)"] $blMetin = if (-not $Yonetici) { 'yetki gerekli' } elseif (-not $BitLockerYetenek) { 'Belirlenemedi' } elseif ($bl) { if ("$($bl.ProtectionStatus)" -eq 'On') { "Açık (%$($bl.EncryptionPercentage))" } else { 'KAPALI' } } else { 'KAPALI' } [pscustomobject]@{ 'Sürücü' = $v.DeviceID 'Etiket' = Get-Safe { $v.VolumeName } '-' 'Dosya Sistemi' = Get-Safe { $v.FileSystem } '-' 'Toplam' = Boyut $v.Size 'Boş' = Boyut $v.FreeSpace 'Doluluk' = "%$doluOran" 'BitLocker' = $blMetin } if ($doluOran -ge 90) { Bulgu -Seviye 'Kritik' -Baslik "Disk alanı kritik ($($v.DeviceID))" ` -Detay "$($v.DeviceID) sürücüsü %$doluOran dolu, sadece $(Boyut $v.FreeSpace) boş alan var." ` -Oneri 'Temizlik yapılmalı veya kapasite artırılmalı.' } elseif ($doluOran -ge 80) { Bulgu -Seviye 'Uyarı' -Baslik "Disk alanı azalıyor ($($v.DeviceID))" ` -Detay "$($v.DeviceID) sürücüsü %$doluOran dolu." ` -Oneri 'Kısa vadede temizlik planlanmalı.' } } ) # Disk şifrelemesi: taşınabilir cihazda kritik, masaüstünde bilgi seviyesinde. # Masaüstü de çalınabilir ya da disk sökülebilir; ama risk profili farklı. # $BitLockerYetenek koşulu şart: komut yoksa "kapalı" bilgisi elimizde YOK, # olmayan bilgiden bulgu üretilmez. if ($Yonetici -and $BitLockerYetenek -and -not $SanalMi) { $sysBl = $bitlocker["$($env:SystemDrive)"] if (-not $sysBl -or "$($sysBl.ProtectionStatus)" -ne 'On') { if ($Dizustu) { Bulgu -Seviye 'Kritik' -Baslik 'Taşınabilir cihazda disk şifrelemesi kapalı' ` -Detay "$($env:SystemDrive) sürücüsünde BitLocker kapalı. Cihaz çalınırsa veriler açıkta." ` -Oneri 'BitLocker etkinleştirilmeli, kurtarma anahtarı merkezi olarak saklanmalı.' } else { Bulgu -Seviye 'Bilgi' -Baslik 'Disk şifrelemesi kapalı' ` -Detay "$($env:SystemDrive) sürücüsünde BitLocker kapalı. Masaüstünde risk dizüstüne göre düşüktür, ancak disk sökülüp başka cihaza takılırsa veriler okunabilir." ` -Oneri 'Hassas veri barındıran iş istasyonlarında BitLocker değerlendirilmeli.' } } } $INV['Diğer Sürücüler'] = @( foreach ($v in (Get-Safe { Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=2 OR DriveType=4 OR DriveType=5' } @())) { $t = switch ([int]$v.DriveType) { 2 {'Çıkarılabilir (USB)'} 4 {'Ağ Sürücüsü'} 5 {'CD/DVD'} default {'Diğer'} } $harf = "$(Get-Safe { $v.DeviceID } '')" $harfNo = SurucununDiski $harf # Çıkarılabilir sürücülerde 'Hedef' sütunu zaten boştur; aracın kendi # belleği buraya düştüyse orada işaretlensin. $hedef = if ([int]$v.DriveType -eq 2 -and (DiskHariciMi $harfNo)) { HariciNot $harfNo } else { Get-Safe { $v.ProviderName } '-' } [pscustomobject]@{ 'Sürücü' = $v.DeviceID 'Tip' = $t 'Etiket' = Get-Safe { $v.VolumeName } '-' 'Hedef' = $hedef 'Toplam' = Boyut $v.Size } } # "Yerel disk" gibi görünen harici sürücüler yukarıda ayıklandı, buraya # ekleniyor. Bilgi kaybolmuyor, sadece doğru bölüme yazılıyor. foreach ($h in $hariciMantiksal) { $h } ) # Fiziksel tarafta harici bulunduysa ayrı bir bölüm açılıyor. Hiç yoksa # bölüm hiç oluşturulmuyor - boş tablo raporu kalabalıklaştırır. if ($HariciDepolama.Count -gt 0) { $INV['Harici / Çıkarılabilir Depolama'] = @($HariciDepolama) } AdimBitti # --------------------------------------------------------------------------- # 7. EKRAN KARTI VE MONİTÖRLER # --------------------------------------------------------------------------- Adim "Ekran kartı ve monitörler" $INV['Ekran Kartı'] = @( foreach ($g in (Get-Safe { Get-CimInstance Win32_VideoController } @())) { $vram = Get-Safe { [uint32]$g.AdapterRAM } 0 [pscustomobject]@{ 'Model' = Get-Safe { $g.Name } '-' 'Bellek' = if ($vram -gt 0) { Boyut $vram } else { '-' } 'Çözünürlük' = Get-Safe { "$($g.CurrentHorizontalResolution) x $($g.CurrentVerticalResolution) @ $($g.CurrentRefreshRate) Hz" } '-' 'Sürücü Sürümü' = Get-Safe { $g.DriverVersion } '-' 'Sürücü Tarihi' = TarihKisa (Get-Safe { $g.DriverDate }) 'Durum' = Get-Safe { $g.Status } '-' } } ) $UreticiKod = @{ 'ACR'='Acer';'AOC'='AOC';'APP'='Apple';'AUO'='AU Optronics';'BNQ'='BenQ';'CMN'='Chi Mei'; 'DEL'='Dell';'GSM'='LG';'HPN'='HP';'HWP'='HP';'IVM'='Iiyama';'LEN'='Lenovo';'LGD'='LG Display'; 'MSI'='MSI';'NEC'='NEC';'PHL'='Philips';'SAM'='Samsung';'SDC'='Samsung Display';'SEC'='Samsung'; 'SNY'='Sony';'VSC'='ViewSonic';'BOE'='BOE';'CMO'='Chi Mei';'GBT'='Gigabyte';'ASU'='Asus';'AUS'='Asus' } $INV['Monitörler'] = @( foreach ($m in (Get-Safe { Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorID } @())) { $kod = Uint16Metin $m.ManufacturerName $ureticiAd = if ($kod -and $UreticiKod.ContainsKey($kod)) { $UreticiKod[$kod] } else { $kod } $bId = "$($m.InstanceName)" $boyutObj = Get-Safe { Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorBasicDisplayParams | Where-Object { $_.InstanceName -eq $bId } | Select-Object -First 1 } $inc = if ($boyutObj -and $boyutObj.MaxHorizontalImageSize) { $w = [double]$boyutObj.MaxHorizontalImageSize $h = [double]$boyutObj.MaxVerticalImageSize $koseg = [math]::Round([math]::Sqrt(($w * $w) + ($h * $h)) / 2.54, 0) '{0} inç (yaklaşık)' -f $koseg } else { '-' } [pscustomobject]@{ 'Üretici' = if ($ureticiAd) { $ureticiAd } else { '-' } 'Model' = Get-Safe { Uint16Metin $m.UserFriendlyName } '-' 'Model Kodu' = Get-Safe { Uint16Metin $m.ProductCodeID } '-' 'Seri No' = Get-Safe { Uint16Metin $m.SerialNumberID } '-' 'Üretim Yılı' = Get-Safe { "$($m.YearOfManufacture) (hafta $($m.WeekOfManufacture))" } '-' 'Ekran Boyutu' = $inc } } ) AdimBitti # --------------------------------------------------------------------------- # 8. AĞ YAPILANDIRMASI # --------------------------------------------------------------------------- Adim "Ağ yapılandırması" $INV['Ağ Adaptörleri'] = @( foreach ($n in (Get-Safe { Get-CimInstance Win32_NetworkAdapterConfiguration -Filter 'IPEnabled=TRUE' } @())) { $adp = Get-Safe { Get-CimInstance Win32_NetworkAdapter -Filter "Index=$($n.Index)" } [pscustomobject]@{ 'Adaptör' = Get-Safe { $n.Description } '-' 'Bağlantı Adı' = Get-Safe { $adp.NetConnectionID } '-' 'MAC Adresi' = Get-Safe { $n.MACAddress } '-' 'IPv4' = Get-Safe { ($n.IPAddress | Where-Object { $_ -notmatch ':' }) -join ', ' } '-' 'Alt Ağ Maskesi' = Get-Safe { ($n.IPSubnet | Where-Object { $_ -match '^\d' }) -join ', ' } '-' 'Ağ Geçidi' = Get-Safe { $n.DefaultIPGateway -join ', ' } '-' 'DNS' = Get-Safe { $n.DNSServerSearchOrder -join ', ' } '-' 'DHCP' = Get-Safe { if ($n.DHCPEnabled) { "Açık (sunucu: $($n.DHCPServer))" } else { 'Kapalı (statik IP)' } } '-' 'Hız' = Get-Safe { if ($adp.Speed) { "$([math]::Round($adp.Speed/1000000,0)) Mbps" } else { '-' } } '-' } } ) $INV['Tüm Ağ Kartları'] = @( foreach ($a in (Get-Safe { Get-NetAdapter } @())) { [pscustomobject]@{ 'Ad' = $a.Name 'Açıklama' = $a.InterfaceDescription 'MAC' = $a.MacAddress 'Durum' = switch ("$($a.Status)") { 'Up' {'Bağlı'} 'Down' {'Bağlı değil'} 'Disabled' {'Devre dışı'} default { "$($a.Status)" } } 'Hız' = Get-Safe { "$($a.LinkSpeed)" } '-' 'Tip' = Get-Safe { "$($a.MediaType)" } '-' } } ) $disIp = Get-Safe { (Invoke-WebRequest -Uri 'https://api.ipify.org' -UseBasicParsing -TimeoutSec 5).Content.Trim() } 'Alınamadı' $INV['Ağ Özeti'] = [pscustomobject]@{ 'Ana Bilgisayar Adı' = Get-Safe { [System.Net.Dns]::GetHostName() } '-' 'DNS Son Eki' = Get-Safe { (Get-CimInstance Win32_ComputerSystem).Domain } '-' 'Dış IP Adresi' = $disIp 'Aktif Bağlantı' = Get-Safe { (Get-NetConnectionProfile | Select-Object -First 1).Name } '-' 'Ağ Kategorisi' = Get-Safe { "$((Get-NetConnectionProfile | Select-Object -First 1).NetworkCategory)" } '-' 'Proxy' = Get-Safe { $p = Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' if ($p.ProxyEnable -eq 1) { "Açık: $($p.ProxyServer)" } else { 'Kapalı' } } '-' } AdimBitti # --------------------------------------------------------------------------- # 9. GÜVENLİK DURUMU # --------------------------------------------------------------------------- Adim "Güvenlik durumu" $avListe = @(Get-Safe { Get-CimInstance -Namespace root\SecurityCenter2 -ClassName AntiVirusProduct } @()) $INV['Güvenlik Yazılımları'] = @( foreach ($av in $avListe) { $st = Get-Safe { [int]$av.productState } 0 $hex = '{0:x6}' -f $st $etkin = $hex.Substring(2,2) -in @('10','11') $guncel = $hex.Substring(4,2) -eq '00' [pscustomobject]@{ 'Ürün' = Get-Safe { $av.displayName } '-' 'Gerçek Zamanlı Koruma' = if ($etkin) { 'Açık' } else { 'KAPALI' } 'İmza Güncelliği' = if ($guncel) { 'Güncel' } else { 'GÜNCEL DEĞİL' } 'Yol' = Get-Safe { $av.pathToSignedProductExe } '-' } if (-not $etkin) { Bulgu -Seviye 'Kritik' -Baslik 'Antivirüs koruması kapalı' ` -Detay "$($av.displayName) yüklü ancak gerçek zamanlı koruma kapalı." ` -Oneri 'Koruma açılmalı veya ürün kaldırılıp çalışan bir çözüm kurulmalı.' } elseif (-not $guncel) { Bulgu -Seviye 'Uyarı' -Baslik 'Antivirüs imzaları güncel değil' ` -Detay "$($av.displayName) virüs tanım dosyaları güncel değil." ` -Oneri 'Güncelleme çalıştırılmalı, internet erişimi kontrol edilmeli.' } } ) if ($avListe.Count -eq 0) { Bulgu -Seviye 'Kritik' -Baslik 'Antivirüs tespit edilemedi' ` -Detay 'Güvenlik Merkezi kayıtlı bir antivirüs ürünü bildirmiyor.' ` -Oneri 'En azından Microsoft Defender etkinleştirilmeli.' } $INV['Güvenlik Duvarı'] = @( foreach ($fw in (Get-Safe { Get-NetFirewallProfile } @())) { [pscustomobject]@{ 'Profil' = switch ("$($fw.Name)") { 'Domain' {'Etki Alanı'} 'Private' {'Özel'} 'Public' {'Genel'} default { "$($fw.Name)" } } 'Durum' = if ($fw.Enabled) { 'Açık' } else { 'KAPALI' } 'Gelen Bağlantı' = "$($fw.DefaultInboundAction)" 'Giden Bağlantı' = "$($fw.DefaultOutboundAction)" } if (-not $fw.Enabled) { Bulgu -Seviye 'Kritik' -Baslik "Güvenlik duvarı kapalı ($($fw.Name))" ` -Detay "$($fw.Name) profili için Windows Güvenlik Duvarı devre dışı." ` -Oneri 'Güvenlik duvarı açılmalı.' } } ) $tpm = if ($Yonetici) { Get-Safe { Get-Tpm } } else { $null } <# TPM SÜRÜMÜ - Get-Tpm neden yetmiyor: Get-Tpm'in ManufacturerVersion alanı ÜRETİCİ ÜRÜN YAZILIMI sürümüdür (ör. 7.2.3.1); TPM spesifikasyon sürümü (1.2 / 2.0) DEĞİLDİR. Eskiden ekrana ve CSV'ye bu değer "sürüm" diye basılıyordu, yani yanlış bilgiydi. Windows 11 uygunluğunu belirleyen spesifikasyon sürümüdür. Doğru kaynak Win32_Tpm sınıfının SpecVersion alanı ("2.0, 0, 1.38" biçiminde gelir; ilk parça alınır). Ayrı bir WMI ad alanındadır ve yönetici ister; okunamazsa ürün yazılımı sürümü yazılır ama adı doğru söylenir. #> $tpmSurum = if ($Yonetici) { Get-Safe { $tw = Get-CimInstance -Namespace 'root\CIMV2\Security\MicrosoftTpm' -ClassName Win32_Tpm -ErrorAction Stop if ($tw.SpecVersion) { (("$($tw.SpecVersion)" -split ',')[0]).Trim() } else { $null } } } else { $null } $secureBoot = if ($Yonetici) { Get-Safe { if (Confirm-SecureBootUEFI) { 'Açık' } else { 'KAPALI' } } 'Desteklenmiyor (Legacy BIOS)' } else { 'yetki gerekli' } $rdp = Get-Safe { if ((Get-ItemProperty 'HKLM:\System\CurrentControlSet\Control\Terminal Server').fDenyTSConnections -eq 0) { 'Açık' } else { 'Kapalı' } } '-' $smb1 = Get-Safe { "$((Get-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -ErrorAction Stop).State)" } 'Okunamadı' $smb1TR = switch ($smb1) { 'Enabled' { 'ETKİN' } 'Disabled' { 'Kapalı' } default { $smb1 } } $uac = Get-Safe { if ((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System').EnableLUA -eq 1) { 'Açık' } else { 'KAPALI' } } '-' $INV['Güvenlik Özellikleri'] = [pscustomobject]@{ 'TPM Mevcut' = if ($null -ne $tpm) { if ($tpm.TpmPresent) { if ($tpmSurum) { "Evet (TPM $tpmSurum)" } else { "Evet (ürün yazılımı $(Get-Safe { $tpm.ManufacturerVersion } '-'))" } } else { 'Hayır' } } else { if ($Yonetici) { 'Okunamadı' } else { 'yetki gerekli' } } 'TPM Hazır' = if ($null -ne $tpm) { if ($tpm.TpmReady) { 'Evet' } else { 'Hayır' } } else { '-' } 'Secure Boot' = $secureBoot 'UEFI / BIOS' = Get-Safe { if ($env:firmware_type) { $env:firmware_type } elseif ((Get-Item 'HKLM:\System\CurrentControlSet\Control\SecureBoot\State' -ErrorAction Stop)) { 'UEFI' } else { 'Legacy BIOS' } } 'Belirlenemedi' 'UAC (Kullanıcı Hesabı Denetimi)' = $uac 'Uzak Masaüstü (RDP)' = $rdp 'SMBv1 (eski protokol)' = $smb1TR 'Windows Defender' = Get-Safe { $d = Get-MpComputerStatus -ErrorAction Stop "Gerçek zamanlı: $(if($d.RealTimeProtectionEnabled){'Açık'}else{'Kapalı'}) | İmza: $(TarihKisa $d.AntivirusSignatureLastUpdated)" } '-' } if ($smb1 -eq 'Enabled') { Bulgu -Seviye 'Kritik' -Baslik 'SMBv1 protokolü etkin' ` -Detay 'Eski ve güvensiz SMBv1 protokolü açık (WannaCry/EternalBlue zafiyet yüzeyi).' ` -Oneri 'Eski bir cihaz zorunlu kılmıyorsa SMBv1 kaldırılmalı.' } if ($uac -eq 'KAPALI') { Bulgu -Seviye 'Uyarı' -Baslik 'UAC devre dışı' ` -Detay 'Kullanıcı Hesabı Denetimi kapalı, zararlı yazılım onay istemeden yetki yükseltebilir.' ` -Oneri 'UAC varsayılan seviyeye alınmalı.' } if ($rdp -eq 'Açık') { Bulgu -Seviye 'Bilgi' -Baslik 'Uzak Masaüstü açık' ` -Detay 'RDP bağlantılarına izin veriliyor.' ` -Oneri 'İnternete açık değilse sorun yok; dışarıdan erişiliyorsa VPN arkasına alınmalı.' } AdimBitti # --------------------------------------------------------------------------- # 10. KULLANICILAR VE PAYLAŞIMLAR # --------------------------------------------------------------------------- Adim "Kullanıcılar ve paylaşımlar" $INV['Yerel Kullanıcılar'] = @( foreach ($u in (Get-Safe { Get-LocalUser } @())) { [pscustomobject]@{ 'Kullanıcı' = $u.Name 'Tam Ad' = Get-Safe { $u.FullName } '-' 'Etkin' = if ($u.Enabled) { 'Evet' } else { 'Hayır' } 'Şifre Son Değişim' = TarihKisa (Get-Safe { $u.PasswordLastSet }) 'Şifre Süresiz' = if ($u.PasswordNeverExpires) { 'Evet' } else { 'Hayır' } 'Son Oturum' = TarihKisa (Get-Safe { $u.LastLogon }) 'Açıklama' = Get-Safe { $u.Description } '-' } } ) $adminGrup = Get-Safe { Get-LocalGroup | Where-Object { $_.SID.Value -like '*-544' } | Select-Object -First 1 } $adminUyeler = if ($adminGrup) { @(Get-Safe { Get-LocalGroupMember -Group $adminGrup.Name } @()) } else { @() } $INV['Yönetici Grubu Üyeleri'] = @( foreach ($a in $adminUyeler) { [pscustomobject]@{ 'Üye' = $a.Name 'Tip' = "$($a.ObjectClass)" 'Kaynak' = "$($a.PrincipalSource)" } } ) if ($adminUyeler.Count -gt 3) { Bulgu -Seviye 'Uyarı' -Baslik 'Çok fazla yerel yönetici' ` -Detay "Yöneticiler grubunda $($adminUyeler.Count) üye var." ` -Oneri 'Günlük kullanım hesapları standart kullanıcıya düşürülmeli.' } $INV['Ağ Paylaşımları'] = @( foreach ($s in (Get-Safe { Get-CimInstance Win32_Share } @())) { [pscustomobject]@{ 'Paylaşım Adı' = $s.Name 'Yol' = Get-Safe { $s.Path } '-' 'Tip' = if ($s.Name -match '\$$') { 'Yönetimsel (gizli)' } else { 'Kullanıcı Paylaşımı' } 'Açıklama' = Get-Safe { $s.Description } '-' } } ) AdimBitti # --------------------------------------------------------------------------- # 11. YAZICILAR # --------------------------------------------------------------------------- Adim "Yazıcılar" $INV['Yazıcılar'] = @( foreach ($p in (Get-Safe { Get-CimInstance Win32_Printer } @())) { [pscustomobject]@{ 'Yazıcı' = $p.Name 'Model' = Get-Safe { $p.DriverName } '-' 'Bağlantı' = Get-Safe { $p.PortName } '-' 'Tip' = if ($p.Network) { 'Ağ' } elseif ($p.Local) { 'Yerel' } else { '-' } 'Varsayılan' = if ($p.Default) { 'EVET' } else { '' } 'Paylaşımda' = if ($p.Shared) { "Evet ($($p.ShareName))" } else { 'Hayır' } 'Durum' = Get-Safe { $p.PrinterStatus } '-' } } ) AdimBitti # --------------------------------------------------------------------------- # 12. BATARYA (TAŞINABİLİR CİHAZLAR) # --------------------------------------------------------------------------- if ($Dizustu -or (Get-Safe { Get-CimInstance Win32_Battery })) { Adim "Batarya durumu" $bat = Get-Safe { Get-CimInstance Win32_Battery | Select-Object -First 1 } $bsd = Get-Safe { Get-CimInstance -Namespace root\wmi -ClassName BatteryStaticData | Select-Object -First 1 } $bfc = Get-Safe { Get-CimInstance -Namespace root\wmi -ClassName BatteryFullChargedCapacity | Select-Object -First 1 } $tasarim = Get-Safe { [double]$bsd.DesignedCapacity } 0 $mevcut = Get-Safe { [double]$bfc.FullChargedCapacity } 0 $saglik = if ($tasarim -gt 0 -and $mevcut -gt 0) { [math]::Round(($mevcut / $tasarim) * 100, 0) } else { $null } $kimyaKod = Get-Safe { [int]$bat.Chemistry } 0 $kimya = switch ($kimyaKod) { 1 { 'Diğer' } 2 { 'Bilinmiyor' } 3 { 'Kurşun Asit' } 4 { 'NiCd' } 5 { 'NiMH' } 6 { 'Lityum İyon' } 7 { 'Çinko Hava' } 8 { 'Lityum Polimer' } default { '-' } } $durumKod = Get-Safe { [int]$bat.BatteryStatus } 0 $batDurum = switch ($durumKod) { 1 { 'Boşalıyor' } 2 { 'Prizde / Şarjda' } 3 { 'Tam Dolu' } 4 { 'Düşük' } 5 { 'Kritik' } default { '-' } } $INV['Batarya'] = [pscustomobject]@{ 'Batarya' = Get-Safe { $bat.Name } '-' 'Kimyasal Tip' = $kimya 'Tasarım Kapasitesi' = if ($tasarim) { "$tasarim mWh" } else { '-' } 'Mevcut Kapasite' = if ($mevcut) { "$mevcut mWh" } else { '-' } 'Batarya Sağlığı' = if ($null -ne $saglik) { "%$saglik" } else { 'Hesaplanamadı' } 'Şarj Seviyesi' = Get-Safe { "%$($bat.EstimatedChargeRemaining)" } '-' 'Durum' = $batDurum } if ($null -ne $saglik -and $saglik -lt 70) { Bulgu -Seviye 'Uyarı' -Baslik 'Batarya yıprandı' ` -Detay "Batarya sağlığı %$saglik (tasarım kapasitesinin altında)." ` -Oneri 'Batarya değişimi bütçelenebilir; cihaz prize bağımlı hale gelmiş olabilir.' } AdimBitti } # --------------------------------------------------------------------------- # 13. KURULU YAZILIMLAR # --------------------------------------------------------------------------- if (-not $YazilimAtla) { Adim "Kurulu yazılımlar" $regYollar = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' ) $ham = @(Get-Safe { Get-ItemProperty $regYollar } @()) $yazilimlar = @( $ham | Where-Object { $_.DisplayName -and -not $_.SystemComponent -and -not $_.ReleaseType -and -not $_.ParentKeyName -and $_.DisplayName -notmatch '^(Update for|Security Update|Hotfix|Service Pack)' } | ForEach-Object { [pscustomobject]@{ 'Yazılım' = $_.DisplayName.Trim() 'Sürüm' = Get-Safe { "$($_.DisplayVersion)" } '-' 'Yayıncı' = Get-Safe { "$($_.Publisher)" } '-' # InvariantCulture şart: $null (=CurrentCulture) verilirse # Gregoryen olmayan takvim kullanan sistemlerde (Tayland Budist, # Suudi Hicri) yıl yanlış yorumlanır. 'Kurulum Tarihi' = Get-Safe { if ($_.InstallDate -match '^\d{8}$') { [datetime]::ParseExact($_.InstallDate, 'yyyyMMdd', [System.Globalization.CultureInfo]::InvariantCulture).ToString('dd.MM.yyyy') } else { '-' } } '-' 'Boyut' = Get-Safe { if ($_.EstimatedSize) { Boyut ([double]$_.EstimatedSize * 1024) } else { '-' } } '-' } } | Sort-Object -Property 'Yazılım' -Unique ) $INV['Kurulu Yazılımlar'] = $yazilimlar AdimBitti "$($yazilimlar.Count) yazılım" $eskiler = $yazilimlar | Where-Object { $_.'Yazılım' -match 'Adobe Flash|Java 6|Java 7|Internet Explorer|Silverlight|WinRAR 4|Office 2007|Office 2010' } foreach ($e in $eskiler) { Bulgu -Seviye 'Uyarı' -Baslik 'Desteği biten yazılım' ` -Detay "$($e.'Yazılım') $($e.'Sürüm') kurulu. Üretici desteği sona ermiş." ` -Oneri 'Kaldırılmalı veya güncel sürümle değiştirilmeli.' } } else { $INV['Kurulu Yazılımlar'] = @() } # --------------------------------------------------------------------------- # 14. BAŞLANGIÇ + GÜNCELLEMELER # --------------------------------------------------------------------------- Adim "Başlangıç ve güncellemeler" $INV['Başlangıçta Çalışanlar'] = @( foreach ($s in (Get-Safe { Get-CimInstance Win32_StartupCommand } @())) { [pscustomobject]@{ 'Ad' = $s.Name 'Komut' = Get-Safe { $s.Command } '-' 'Kullanıcı' = Get-Safe { $s.User } '-' 'Konum' = Get-Safe { $s.Location } '-' } } ) $hotfix = @(Get-Safe { Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 10 } @()) $INV['Son Güncellemeler'] = @( foreach ($h in $hotfix) { [pscustomobject]@{ 'KB No' = $h.HotFixID 'Tip' = Get-Safe { $h.Description } '-' 'Kurulum Tarihi' = TarihKisa (Get-Safe { $h.InstalledOn }) 'Kuran' = Get-Safe { $h.InstalledBy } '-' } } ) $sonGuncelleme = Get-Safe { ($hotfix | Select-Object -First 1).InstalledOn } if ($sonGuncelleme) { $gunFark = ((Get-Date) - $sonGuncelleme).Days if ($gunFark -gt 90) { Bulgu -Seviye 'Kritik' -Baslik 'Windows güncellemeleri geride' ` -Detay "Son güncelleme $gunFark gün önce ($(TarihKisa $sonGuncelleme)) kurulmuş." ` -Oneri 'Windows Update çalıştırılmalı, güncelleme politikası gözden geçirilmeli.' } elseif ($gunFark -gt 45) { Bulgu -Seviye 'Uyarı' -Baslik 'Güncellemeler gecikmiş' ` -Detay "Son güncelleme $gunFark gün önce kurulmuş." ` -Oneri 'Güncelleme döngüsü düzenli hale getirilmeli.' } } # --- Windows Update yapılandırması --------------------------------------- # "Güncellemeler geride" bulgusu NE olduğunu söylüyor ama NEDEN'ini söylemiyor. # Sahada asıl soru budur: cihaz unutulmuş mu, bir WSUS sunucusuna mı bağlı, # yoksa güncelleme bilerek mi kapatılmış? Teknisyenin yapacağı iş buna göre # değişir. Bu blok o farkı ortaya koyar. $wuPolYol = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' $WsusAdres = GecerliVeya (Get-Safe { "$((Get-ItemProperty $wuPolYol -Name WUServer -ErrorAction Stop).WUServer)" } '') $noAuto = Get-Safe { [int](Get-ItemProperty "$wuPolYol\AU" -Name NoAutoUpdate -ErrorAction Stop).NoAutoUpdate } -1 $auSecenek = Get-Safe { [int](Get-ItemProperty "$wuPolYol\AU" -Name AUOptions -ErrorAction Stop).AUOptions } -1 # Servis durumu Get-Service yerine registry'den okunuyor: Start=4 "Devre Dışı" # demektir ve bu sayı dilden bağımsızdır. (Get-Service'in StartType enum'u da # İngilizce'dir ama servis hiç yoksa hata verir; registry sessizce boş döner.) $wuStart = Get-Safe { [int](Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\wuauserv' -Name Start -ErrorAction Stop).Start } -1 <# DURAKLATMA - buradaki tuzak: PauseUpdatesExpiryTime, duraklatma süresi DOLDUKTAN SONRA DA eski tarihiyle registry'de kalır. Yalnızca "değer var mı" diye bakılırsa bir kez güncelleme duraklatmış her cihaz sonsuza kadar "duraklatılmış" görünür - yani filonun büyük kısmında sahte alarm. Bu yüzden tarih şimdiki zamanla karşılaştırılıyor. Değer UTC olarak ("2026-06-15T00:00:00Z") saklandığı için karşılaştırma da UTC üzerinden. #> $duraklatmaBitis = Get-Safe { $ham = "$((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings' -Name PauseUpdatesExpiryTime -ErrorAction Stop).PauseUpdatesExpiryTime)" if ([string]::IsNullOrWhiteSpace($ham)) { $null } else { [datetime]::Parse($ham, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::RoundtripKind).ToUniversalTime() } } $null $DuraklatmaAktif = ($null -ne $duraklatmaBitis) -and ($duraklatmaBitis -gt [datetime]::UtcNow) $auMetin = switch ($auSecenek) { 1 { 'Otomatik güncelleme kapalı' } 2 { 'İndirme öncesi bildir' } 3 { 'Otomatik indir, kurulumu bildir' } 4 { 'Otomatik indir ve zamanlanmış kur' } 5 { 'Yerel yönetici seçer' } default { 'Politika tanımlı değil' } } $INV['Windows Update Yapılandırması'] = [pscustomobject]@{ 'Güncelleme Kaynağı' = if ($WsusAdres) { "WSUS / SCCM ($WsusAdres)" } else { 'Microsoft Update (doğrudan)' } 'Otomatik Güncelleme' = if ($noAuto -eq 1) { 'KAPALI (politika ile)' } elseif ($noAuto -eq 0) { 'Açık (politika ile)' } else { 'Politika tanımlı değil' } 'Politika Davranışı' = $auMetin 'Windows Update Servisi' = switch ($wuStart) { 2 { 'Otomatik' } 3 { 'Elle (Manual)' } 4 { 'DEVRE DIŞI' } default { 'Belirlenemedi' } } 'Güncelleme Duraklatma' = if ($DuraklatmaAktif) { "Etkin - $(Tarih ($duraklatmaBitis.ToLocalTime())) tarihine kadar" } elseif ($null -ne $duraklatmaBitis) { 'Yok (geçmiş duraklatma kaydı var, süresi dolmuş)' } else { 'Yok' } 'Son Güncelleme' = if ($sonGuncelleme) { TarihKisa $sonGuncelleme } else { '-' } } if ($wuStart -eq 4) { Bulgu -Seviye 'Kritik' -Baslik 'Windows Update servisi devre dışı bırakılmış' ` -Detay 'wuauserv servisi "Devre Dışı" durumda. Bu haliyle cihaz güvenlik güncellemelerini hiçbir şekilde alamaz; Windows Update ekranı elle çalıştırılsa bile açılmaz.' ` -Oneri 'Servis "Elle" ya da "Otomatik" durumuna alınmalı ve bekleyen güncellemeler kurulmalı.' } if ($noAuto -eq 1 -and $wuStart -ne 4) { Bulgu -Seviye 'Uyarı' -Baslik 'Otomatik güncelleme politika ile kapatılmış' ` -Detay 'NoAutoUpdate politikası 1 olarak ayarlanmış; güncellemeler kendiliğinden inmez, elle başlatılması gerekir.' ` -Oneri 'Cihaz bir RMM/WSUS çözümüyle merkezî olarak yönetiliyorsa bu ayar normaldir. Yönetilmiyorsa politika kaldırılmalı.' } if ($DuraklatmaAktif) { Bulgu -Seviye 'Uyarı' -Baslik 'Güncellemeler duraklatılmış' ` -Detay "Windows Update $(Tarih ($duraklatmaBitis.ToLocalTime())) tarihine kadar duraklatılmış durumda." ` -Oneri 'Duraklatma bilinçli değilse kaldırılmalı; bilinçliyse bitiş tarihi takvime alınmalı.' } $rebootGerekli = @() if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') { $rebootGerekli += 'Bileşen kurulumu' } if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') { $rebootGerekli += 'Windows Update' } if (Get-Safe { (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name PendingFileRenameOperations -ErrorAction Stop) }) { $rebootGerekli += 'Bekleyen dosya işlemi' } $INV['Sistem Durumu'] = [pscustomobject]@{ 'Yeniden Başlatma Gerekli' = if ($rebootGerekli.Count) { 'EVET - ' + ($rebootGerekli -join ', ') } else { 'Hayır' } # Get-Service .NET enum döndürür (Status / StartType) - adları her dilde # İngilizce'dir. WMI'nin StartMode / State metin alanları yerine bu # kullanılıyor, çünkü onlar bazı dillerde çevrilebiliyor. 'Çalışan Servis Sayısı' = Get-Safe { @(Get-Service | Where-Object { $_.Status -eq 'Running' }).Count } '-' 'Otomatik Ama Durmuş Servis' = Get-Safe { @(Get-Service | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -eq 'Stopped' }).Count } '-' 'Başlangıç Öğesi Sayısı' = $INV['Başlangıçta Çalışanlar'].Count 'Sayfa Dosyası' = Get-Safe { $pf = Get-CimInstance Win32_PageFileUsage | Select-Object -First 1; "$($pf.Name) - $($pf.AllocatedBaseSize) MB" } '-' } if ($uptime -and $uptime.Days -gt 30) { Bulgu -Seviye 'Uyarı' -Baslik 'Cihaz çok uzun süredir yeniden başlatılmamış' ` -Detay "Çalışma süresi $($uptime.Days) gün. Güncellemeler tam uygulanmamış olabilir." ` -Oneri 'Planlı yeniden başlatma yapılmalı.' } if ($rebootGerekli.Count) { Bulgu -Seviye 'Uyarı' -Baslik 'Yeniden başlatma bekliyor' ` -Detay (($rebootGerekli -join ', ') + ' işlemleri için yeniden başlatma gerekiyor.') ` -Oneri 'Uygun bir zamanda cihaz yeniden başlatılmalı.' } AdimBitti # --------------------------------------------------------------------------- # 15. AYGIT SORUNLARI VE OTOMATİK OTURUM AÇMA # --------------------------------------------------------------------------- Adim "Aygıt sorunları ve oturum açma" <# AYGIT YÖNETİCİSİ HATALARI Teknisyenin cihaza oturduğunda ilk baktığı yerlerden biri Aygıt Yöneticisi'dir: sarı ünlem işaretli aygıtlar eksik sürücünün doğrudan kanıtıdır ve yeni kurulan bir cihazın "eksik teslim" edildiğini gösterir. Bu bilgi başka hiçbir bölümde yok. İKİ AYRI LİSTE tutuluyor, çünkü ikisi aynı şey değil: - Sorunlu : sürücü yok / aygıt çalışmıyor -> gerçek iş - Devre dışı : kod 22 ve 32; birileri BİLEREK kapatmış -> bulgu değil, bilgi Aynı listede gösterilseler teknisyen bilinçli kapatılmış bir aygıtı arıza sanardı. ÖNEMLİ: "sorgulanamadı" ile "sorun yok" ayrı tutuluyor. Sorgu -ErrorAction Stop ile yazıldı; hata yutulup boş liste dönseydi bölüm sonsuza kadar "sorun yok" görünür, arızalı cihazda bile sessiz kalırdı. Filtreleme WQL tarafında yapılıyor (-Filter). Tüm aygıtları çekip PowerShell tarafında elemek yüzlerce nesne demektir ve sahada gözle görülür yavaşlatır. #> $aygitKodMetin = @{ 1 = 'Aygıt doğru yapılandırılmamış' 10 = 'Aygıt başlatılamıyor' 12 = 'Yeterli boş kaynak bulunamıyor' 14 = 'Yeniden başlatma gerekiyor' 18 = 'Sürücüler yeniden kurulmalı' 19 = 'Kayıt defteri kaydı bozuk' 21 = 'Sistem aygıtı kaldırıyor' 22 = 'Aygıt devre dışı bırakılmış' 24 = 'Aygıt yok ya da düzgün çalışmıyor' 28 = 'Sürücü kurulu değil' 31 = 'Aygıt düzgün çalışmıyor' 32 = 'Başlangıç tipi devre dışı' 37 = 'Sürücü başlatılamadı' 39 = 'Sürücü bozuk ya da eksik' 43 = 'Windows aygıtı durdurdu (aygıt sorun bildirdi)' 45 = 'Aygıt şu anda bağlı değil' } $AygitSorgulandi = $true $aygitHam = @() try { $aygitHam = @(Get-CimInstance Win32_PnPEntity -Filter 'ConfigManagerErrorCode <> 0' -ErrorAction Stop) } catch { $AygitSorgulandi = $false } # Windows 7'de eşleşmiş her telefon kalıcı bir "Bluetooth Çevre Birimi Aygıtı" # düğümü bırakır ve bu düğüm sürekli kod 28 verir. Gerçek bir arıza değildir; # listeye alınırsa her Win7 cihazında sahte "eksik sürücü" bulgusu üretir. $btArtik = @($aygitHam | Where-Object { $_.ConfigManagerErrorCode -eq 28 -and "$($_.DeviceID)" -like 'BTHENUM\*' }).Count $aygitSorunlu = @($aygitHam | Where-Object { $_.ConfigManagerErrorCode -notin @(22, 32) -and -not ($_.ConfigManagerErrorCode -eq 28 -and "$($_.DeviceID)" -like 'BTHENUM\*') }) $aygitKapali = @($aygitHam | Where-Object { $_.ConfigManagerErrorCode -in @(22, 32) }) # Sorgu başarısızsa bölüm BOŞ bırakılmaz: boş tablo "Kayıt bulunamadı" diye # görünür ve bu "sorun yok" gibi okunur. Neden bakılamadığı açıkça yazılır. $INV['Sorunlu Aygıtlar'] = if (-not $AygitSorgulandi) { @([pscustomobject]@{ 'Aygıt' = 'SORGULANAMADI' 'Sınıf' = '-' 'Üretici' = '-' 'Sorun' = 'Aygıt listesi okunamadı; bu bölümün boş olması "sorun yok" anlamına gelmez.' }) } else { @( foreach ($a in $aygitSorunlu) { $kod = [int]$a.ConfigManagerErrorCode [pscustomobject]@{ 'Aygıt' = GecerliVeya (Get-Safe { "$($a.Name)" } '') 'Sınıf' = Get-Safe { "$($a.PNPClass)" } '-' 'Üretici' = Get-Safe { "$($a.Manufacturer)" } '-' 'Sorun' = "Kod $kod - $(if ($aygitKodMetin.ContainsKey($kod)) { $aygitKodMetin[$kod] } else { 'Tanımsız hata' })" } } ) } $INV['Devre Dışı Aygıtlar'] = @( foreach ($a in $aygitKapali) { $kod = [int]$a.ConfigManagerErrorCode [pscustomobject]@{ 'Aygıt' = GecerliVeya (Get-Safe { "$($a.Name)" } '') 'Sınıf' = Get-Safe { "$($a.PNPClass)" } '-' 'Durum' = "Kod $kod - $(if ($aygitKodMetin.ContainsKey($kod)) { $aygitKodMetin[$kod] } else { 'Devre dışı' })" } } ) if (-not $AygitSorgulandi) { Bulgu -Seviye 'Bilgi' -Baslik 'Aygıt listesi sorgulanamadı' ` -Detay 'Win32_PnPEntity sorgusu hata verdi; eksik sürücü kontrolü bu cihazda yapılamadı. Bölümün boş olması "sorun yok" anlamına GELMEZ.' ` -Oneri 'Aygıt Yöneticisi elle açılıp sarı ünlem işaretli aygıtlar kontrol edilmeli.' } elseif ($aygitSorunlu.Count -gt 0) { $eksikSurucu = @($aygitSorunlu | Where-Object { $_.ConfigManagerErrorCode -in @(28, 39, 37) }).Count # Sanal makinelerde guest-tools kurulmadan 3-5 emüle aygıt kod 28 verir; # bu fiziksel cihazdaki eksik sürücüyle aynı ağırlıkta değildir. $seviye = if ($SanalMi) { 'Bilgi' } else { 'Uyarı' } Bulgu -Seviye $seviye -Baslik "Aygıt Yöneticisi'nde $($aygitSorunlu.Count) sorunlu aygıt var" ` -Detay ("Cihazda $($aygitSorunlu.Count) aygıt hata durumunda" + $(if ($eksikSurucu -gt 0) { ", bunların $eksikSurucu tanesinin sürücüsü kurulu değil" } else { '' }) + '. Ayrıntılı liste raporun "Sorunlu Aygıtlar" bölümündedir.' + $(if ($SanalMi) { ' Cihaz sanal olduğu için bu aygıtların bir kısmı sanallaştırma araçları kurulmadığından olağandır.' } else { '' })) ` -Oneri 'Eksik sürücüler üreticinin destek sayfasından kurulmalı; cihaz yeni teslim edildiyse eksik teslim olarak bildirilmeli.' } <# OTOMATİK OTURUM AÇMA (AutoAdminLogon) Açıksa cihaz parola sorulmadan açılır: fiziksel erişimi olan herkes oturuma girer, disk şifrelemesi de büyük ölçüde anlamsızlaşır. PAROLA DEĞERİ HİÇBİR KOŞULDA OKUNMAZ. Yalnızca DefaultPassword kaydının VAR OLUP OLMADIĞINA bakılır - çünkü varsa parola registry'de DÜZ METİN duruyor demektir ve asıl bulgu budur. Get-ItemProperty da -Name ile hedefli çağrılıyor; parametresiz çağrılsaydı parola dahil bütün değerler belleğe okunurdu. Kiosk / POS / tabela cihazlarında bu ayar bilinçli bir tercihtir ve script bunu ANLAYAMAZ; bu yüzden metin kesin konuşmuyor, teyit istiyor. #> $winlogonYol = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' $autoLogon = Get-Safe { "$((Get-ItemProperty $winlogonYol -Name AutoAdminLogon -ErrorAction Stop).AutoAdminLogon)" } '' $autoKullanici = GecerliVeya (Get-Safe { "$((Get-ItemProperty $winlogonYol -Name DefaultUserName -ErrorAction Stop).DefaultUserName)" } '') $autoAlan = GecerliVeya (Get-Safe { "$((Get-ItemProperty $winlogonYol -Name DefaultDomainName -ErrorAction Stop).DefaultDomainName)" } '') $duzParolaVar = Get-Safe { (Get-Item $winlogonYol -ErrorAction Stop).GetValueNames() -contains 'DefaultPassword' } $false $AutoLogonAcik = ($autoLogon -eq '1') $INV['Oturum Açma Ayarları'] = [pscustomobject]@{ 'Otomatik Oturum Açma' = if ($AutoLogonAcik) { 'AÇIK' } elseif ($autoLogon -eq '0') { 'Kapalı' } else { 'Tanımlı değil' } 'Otomatik Açılan Hesap' = if ($AutoLogonAcik -and $autoKullanici) { if ($autoAlan) { "$autoAlan\$autoKullanici" } else { $autoKullanici } } else { '-' } 'Registry''de Düz Parola' = if ($duzParolaVar) { 'EVET - parola şifrelenmemiş duruyor' } else { 'Hayır' } 'Son Oturum Açan' = Get-Safe { $cs.UserName } '-' } if ($AutoLogonAcik) { <# DİKKAT - hesap adının tırnakları burada, metnin DIŞINDA hazırlanıyor: Genişleyen bir dizgenin $(...) bloğu içinde KAÇIŞLI çift tırnak kullanmak ("`"$ad`"") PowerShell'de ayrıştırma hatasıdır. Bu satırlar bir süre öyle yazılmıştı ve script hiç çalışmıyordu - hata çalışma anında değil, daha ilk satır işletilmeden ayrıştırmada oluşuyor, yani araç tamamen açılmıyordu. Tırnağı metne gömmeyin. #> $autoAd = if ($autoKullanici) { '"' + $autoKullanici + '"' } else { $null } if ($duzParolaVar) { $adKritik = if ($autoAd) { $autoAd } else { 'bir' } Bulgu -Seviye 'Kritik' -Baslik 'Otomatik oturum açma açık ve parola düz metin saklanıyor' ` -Detay "Cihaz açılışta parola sormadan $adKritik hesabına giriyor ve bu hesabın parolası kayıt defterinde ŞİFRELENMEMİŞ olarak duruyor. Cihaza fiziksel erişebilen herkes hem oturuma girer hem parolayı okuyabilir." ` -Oneri 'Otomatik oturum açma kapatılmalı. Kiosk/tabela gibi zorunlu bir kullanım varsa parola registry''de düz metin yerine Sysinternals Autologon aracıyla saklanmalı ve hesap yetkileri kısıtlanmalı.' } else { $seviye = if ($Dizustu) { 'Uyarı' } else { 'Bilgi' } $adBilgi = if ($autoAd) { $autoAd } else { 'kayıtlı' } $ekNot = if ($Dizustu) { ' Cihaz taşınabilir olduğu için kaybolma/çalınma durumunda veriye doğrudan erişilir.' } else { '' } Bulgu -Seviye $seviye -Baslik 'Otomatik oturum açma etkin görünüyor' ` -Detay "Cihaz açılışta parola sormadan $adBilgi hesabına girecek şekilde ayarlanmış.$ekNot" ` -Oneri 'Kiosk, tabela ya da üretim terminali gibi bilinçli bir kullanım değilse kapatılmalı. Bilinçliyse müşteriye teyit ettirilip envantere not düşülmeli.' } } AdimBitti # --------------------------------------------------------------------------- # 16. KLON / İMAJ KONTROLÜ # --------------------------------------------------------------------------- Adim "Klon / imaj kontrolü" <# BİR CİHAZIN KLONLANDIĞI TEK BAŞINA ANLAŞILAMAZ. Sysprep izleri, unattend.xml kalıntısı, "kurulum tarihi donanımdan eski" gibi tek cihaza bakan yöntemlerin hepsi denendi ve elendi; çünkü sinyalleri TERS yönde çalışıyor: markalı cihazların fabrika kurulumu zaten sysprep'lidir (yani sorunsuz her cihazda ateşler), buna karşılık asıl aradığımız ham disk kopyasında (Acronis/Clonezilla) hiçbir iz bırakmaz. Sorun olmayan yerde bağırıp gerçek vakada susan bir kontrol, kontrol değildir. ÇALIŞAN YÖNTEM KARŞILAŞTIRMADIR. Araç zaten aynı USB ile gezilen bütün cihazları tek bir birikimli CSV'de biriktiriyor. O dosya burada "defter" olarak GERİ OKUNUYOR ve şu soru soruluyor: donanım kimliği FARKLI + Windows kurulumu AYNI -> klon / aynı imaj Karar PUANLA değil KURALLA veriliyor. Puan barajlı bir taslak denendi; fabrika sektör imajıyla kurulmuş, kutudan yeni çıkmış iki sağlam cihazı "kesin klon" ilan ettiği için terk edildi. Bu bölüm hiçbir koşulda "Kritik" bulgu üretmez ve hiçbir koşulda "kesin klon" demez: klonlama bir güvenlik açığı değil, envanter ve güncelleme hijyeni konusudur. Defter salt-okunurdur; tek satırı bile değiştirilmez. #> # İki değerin anlamlı biçimde eşit olup olmadığı. Boş/eksik değerler ASLA # eşleşme saymaz - yoksa parmak izi okunamayan iki cihaz birbirinin klonu çıkar. function DegerEsit { param([string]$A, [string]$B) if ([string]::IsNullOrWhiteSpace($A) -or [string]::IsNullOrWhiteSpace($B)) { return $false } return ($A.Trim() -eq $B.Trim()) } $SemaSurumu = '2' $KlonDurumu = 'Tekil' $KlonEslesen = '-' $klonNot = '' $defterOkunan = 0 $defterAtlanan = 0 $defterVarMi = $false # Bu üçü aşağıdaki karşılaştırma döngüsünde doldurulur, ama bulgu koşulları # döngü hiç çalışmadığında da bunlara bakar. Burada peşinen tanımlanıyorlar ki # "tanımsız değişken" durumuna hiç düşülmesin. $enIyiRutbe = 0 $enIyiEslesen = @() $enIyiFarkli = @() # Defterin okunacağı üst sınırlar. Aşılırsa okuma hiç yapılmaz: sahada # teknisyen bekliyor, 20 MB'lık bir CSV'yi ayrıştırmak taramayı dakikalarca # uzatır ve bu kontrol o bedele değmez. $defterEnFazlaBayt = 8MB $defterEnFazlaSatir = 5000 if ($KimlikZayif) { $KlonDurumu = 'Karşılaştırılamadı' $klonNot = 'Bu cihazın kimliği donanımdan üretilemedi. Kurulum aynı çıksa bile cihazın farklı olduğu kanıtlanamayacağı için karşılaştırma yapılmadı.' } elseif (-not $KurulumKimligi -and -not $MakineSidOzeti) { $KlonDurumu = 'Karşılaştırılamadı' $klonNot = 'Kurulum parmak izleri (MachineGuid ve makine SID) bu cihazda okunamadı.' } else { $defterDosyalari = @(Get-Safe { Get-ChildItem -Path $CiktiKlasoru -Filter '_TUM-CIHAZLAR*.csv' -File -ErrorAction Stop } @()) $toplamBayt = ($defterDosyalari | Measure-Object -Property Length -Sum).Sum if (-not $toplamBayt) { $toplamBayt = 0 } if ($defterDosyalari.Count -eq 0) { # Ofisin ilk cihazı. Bu bir sorun değil, bulgu da üretilmez. $klonNot = 'Bu klasördeki ilk cihaz taraması. Karşılaştırılacak önceki kayıt yok; kontrol ikinci cihazdan itibaren kendiliğinden çalışır.' } elseif ($toplamBayt -gt $defterEnFazlaBayt) { $defterVarMi = $true $KlonDurumu = 'Karşılaştırılamadı' $klonNot = "Birikimli dosyalar $([math]::Round($toplamBayt / 1MB, 1)) MB büyüklüğünde ve okuma sınırını ($([math]::Round($defterEnFazlaBayt / 1MB, 0)) MB) aşıyor." } else { $defterVarMi = $true # En güçlü eşleşme kazanır: 3 = klon, 2 = şüpheli (bulgulu), # 1 = şüpheli (sessiz), 0 = eşleşme yok. $enIyiSatir = $null $kimlikKaymasi = $null foreach ($dosya in $defterDosyalari) { $satirlar = @(Get-Safe { Import-Csv -Path $dosya.FullName -Delimiter ';' -Encoding UTF8 -ErrorAction Stop } @()) foreach ($satir in $satirlar) { if ($defterOkunan + $defterAtlanan -ge $defterEnFazlaSatir) { break } $sutunlar = @($satir.PSObject.Properties.Name) # Eski şemayla (1.1 ve öncesi) yazılmış satırlarda parmak izi # sütunları yoktur; bunlar karşılaştırmaya hiç girmez. if ($sutunlar -notcontains 'Kurulum Kimliği') { $defterAtlanan++; continue } $sKimlik = "$($satir.'Kalıcı Kimlik')" $sKurulum = "$($satir.'Kurulum Kimliği')" $sSid = "$($satir.'Makine SID Özeti')" $sBirim = "$($satir.'C: Birim Seri')" $sDamga = "$($satir.'Kurulum Damgası')" $sAd = GecerliVeya "$($satir.'Bilgisayar Adı')" $sTarih = GecerliVeya "$($satir.'Envanter Tarihi')" $sSeri = "$($satir.'Seri No')" if ([string]::IsNullOrWhiteSpace($sKurulum) -and [string]::IsNullOrWhiteSpace($sSid)) { $defterAtlanan++; continue } $defterOkunan++ # Aynı cihazın önceki taraması: sessiz geçilir. if (DegerEsit $sKimlik $CihazKimligi) { continue } # Donanım kimliği kaymış olabilir: seri numarası aynıysa bu # AYNI cihazdır (BIOS güncellemesi, geçici WMI hatası, anakart # değişimi kimliği kaydırabilir). Klon değildir, bulgu üretilmez. if (DegerEsit $sSeri $seriGecerli) { if (-not $kimlikKaymasi) { $kimlikKaymasi = $sAd } continue } $eslesenler = @(); $farkli = @() $birincil = 0; $destek = 0 if (DegerEsit $sKurulum $KurulumKimligi) { $birincil++; $eslesenler += 'kurulum kimliği' } elseif (-not [string]::IsNullOrWhiteSpace($sKurulum)) { $farkli += 'kurulum kimliği' } if (DegerEsit $sSid $MakineSidOzeti) { $birincil++; $eslesenler += 'makine SID' } elseif (-not [string]::IsNullOrWhiteSpace($sSid)) { $farkli += 'makine SID' } if (DegerEsit $sBirim $CBirimSeri) { $destek++; $eslesenler += 'C: birim seri' } elseif (-not [string]::IsNullOrWhiteSpace($sBirim)) { $farkli += 'C: birim seri' } if (DegerEsit $sDamga $KurulumDamgasi) { $destek++; $eslesenler += 'kurulum damgası' } elseif (-not [string]::IsNullOrWhiteSpace($sDamga)) { $farkli += 'kurulum damgası' } # Yalnızca destekleyiciler eşleşiyorsa SESSİZ kalınır: aynı # modelden iki yeni cihaz fabrika sektör imajı yüzünden aynı # birim serisini meşru şekilde taşıyabilir. $rutbe = if ($birincil -ge 2) { 3 } elseif ($birincil -eq 1 -and $destek -ge 1) { 2 } elseif ($birincil -eq 1) { 1 } else { 0 } if ($rutbe -gt $enIyiRutbe) { $enIyiRutbe = $rutbe $enIyiSatir = @{ Ad = $sAd; Tarih = $sTarih } $enIyiEslesen = $eslesenler $enIyiFarkli = $farkli } } } if ($enIyiRutbe -ge 3) { $KlonDurumu = 'Klon' } elseif ($enIyiRutbe -ge 1) { $KlonDurumu = 'Şüpheli' } if ($enIyiSatir) { $KlonEslesen = $enIyiSatir.Ad if ($enIyiSatir.Tarih) { $KlonEslesen += " ($($enIyiSatir.Tarih))" } } elseif ($kimlikKaymasi) { $klonNot = "Envanterdeki `"$kimlikKaymasi`" kaydıyla seri numarası aynı; donanım kimliği değişmiş görünüyor. Bu aynı cihazdır, klon değildir." } if ($defterOkunan -eq 0 -and $defterAtlanan -gt 0) { $KlonDurumu = 'Karşılaştırılamadı' $klonNot = "Birikimli dosyadaki $defterAtlanan kayıt bu sürümden önce yazılmış ve kurulum parmak izi taşımıyor." } } } $INV['Kurulum Kimliği ve Klon Kontrolü'] = [pscustomobject]@{ 'Klon Durumu' = switch ($KlonDurumu) { 'Klon' { 'Aynı kurulum başka bir cihazda da bulundu' } 'Şüpheli' { 'Kısmi örtüşme var' } 'Karşılaştırılamadı' { 'Karşılaştırılamadı' } default { 'Eşleşme yok (tekil kurulum)' } } 'Eşleşen Cihaz' = $KlonEslesen 'Kurulum Kimliği' = if ($KurulumKimligi) { $KurulumKimligi } else { 'Okunamadı' } 'Makine SID Özeti' = if ($MakineSidOzeti) { $MakineSidOzeti } else { 'Okunamadı' } 'C: Birim Seri' = if ($CBirimSeri) { $CBirimSeri } else { '-' } 'Windows Kurulum Anı' = if ($KurulumDamgasi) { $KurulumDamgasi } else { '-' } 'Karşılaştırılan Kayıt' = $defterOkunan 'Atlanan Eski Kayıt' = $defterAtlanan 'Açıklama' = if ($klonNot) { $klonNot } else { 'Kurulum parmak izi envanterdeki diğer cihazlarla karşılaştırıldı.' } } if ($KlonDurumu -eq 'Klon') { # Sanal makinelerde şablondan çoğaltma olağan bir yönetim biçimidir; # fiziksel cihazdaki ham disk kopyasıyla aynı ağırlıkta değildir. $klonSeviye = if ($SanalMi) { 'Bilgi' } else { 'Uyarı' } $klonDetay = "Cihazın donanım kimliği envanterdeki `"$KlonEslesen`" kaydından farklı, ancak Windows kurulum kimliği ve makine SID'i birebir aynı. Bu, kurulumun sysprep yapılmadan (ham disk kopyası olarak) çoğaltıldığını gösterir." if ($SanalMi) { $klonDetay += ' Cihaz sanal olduğu için bu durum bir şablondan çoğaltmaya işaret eder ve sanal ortamda olağandır.' } if ($WsusAdres) { $klonDetay += ' Cihazlar WSUS/SCCM ile yönetildiği için iki cihaz güncelleme sunucusunda tek kayda düşebilir.' } Bulgu -Seviye $klonSeviye -Baslik 'Bu Windows kurulumu başka bir cihazda da kayıtlı' ` -Detay $klonDetay ` -Oneri 'Bu bir güvenlik açığı değil, envanter ve güncelleme hijyeni sorunudur. ÇALIŞAN bir cihazda sysprep çalıştırılmamalıdır; düzeltme, bir sonraki yeniden kurulumda imajın sysprep ile hazırlanmasıyla yapılır.' } elseif ($KlonDurumu -eq 'Şüpheli' -and $enIyiRutbe -ge 2) { Bulgu -Seviye 'Bilgi' -Baslik 'Kurulum izleri başka bir cihazla kısmen örtüşüyor' ` -Detay ("`"$KlonEslesen`" kaydıyla şu değerler aynı: $($enIyiEslesen -join ', ')." + $(if ($enIyiFarkli.Count) { " Şu değerler farklı: $($enIyiFarkli -join ', ')." } else { '' }) + ' Tek başına klon kanıtı değildir; aynı imajdan kurulmuş iki ayrı cihazda da görülür.') ` -Oneri 'İşlem gerekmez. İki cihazda güncelleme ya da etki alanı sorunu yaşanırsa bu kayıt hatırlanmalıdır.' } elseif ($KlonDurumu -eq 'Karşılaştırılamadı' -and $defterVarMi) { Bulgu -Seviye 'Bilgi' -Baslik 'Klon kontrolü yapılamadı' ` -Detay $klonNot ` -Oneri 'Kontrol, birikimli dosya bu sürümle üretilmiş satırlar içerdiğinde kendiliğinden çalışmaya başlar.' } AdimBitti # --------------------------------------------------------------------------- # 17. BULGULARI SIRALA # --------------------------------------------------------------------------- $SeviyeSira = @{ 'Kritik'=1; 'Uyarı'=2; 'Bilgi'=3 } # Büyük harf karşılıkları sabit tutulur: ToUpper() kültüre bağlı çalışır ve # Türkçe'de 'Kritik' -> 'KRİTİK', İngilizce'de 'KRITIK' verir. Özetin her # cihazda aynı görünmesi için tablo kullanılıyor. $SeviyeBuyuk = @{ 'Kritik'='KRİTİK'; 'Uyarı'='UYARI'; 'Bilgi'='BİLGİ' } $BulguSirali = @($Bulgular | Sort-Object { $SeviyeSira["$($_.Seviye)"] }) $INV['Bulgular'] = $BulguSirali $kritikSayi = @($BulguSirali | Where-Object Seviye -eq 'Kritik').Count $uyariSayi = @($BulguSirali | Where-Object Seviye -eq 'Uyarı').Count # --------------------------------------------------------------------------- # ÇIKTI: JSON # --------------------------------------------------------------------------- $dosyaKok = Join-Path $CiktiKlasoru ("{0}_{1}" -f $Bilgisayar, $DamgaDosya) $jsonYol = "$dosyaKok.json" $htmlYol = "$dosyaKok.html" $htmlAdi = Split-Path $htmlYol -Leaf Adim "JSON dosyası yazılıyor" Get-Safe { $INV | ConvertTo-Json -Depth 6 | Out-File -FilePath $jsonYol -Encoding UTF8 } AdimBitti # --------------------------------------------------------------------------- # ÇIKTI: TOPLU CSV (her cihazdan bir satır) # --------------------------------------------------------------------------- Adim "Toplu CSV güncelleniyor" $csvYol = Join-Path $CiktiKlasoru '_TUM-CIHAZLAR.csv' $sistemDisk = $INV['Sürücüler'] | Where-Object { $_.'Sürücü' -eq $env:SystemDrive } | Select-Object -First 1 $diskOzet = ($INV['Fiziksel Diskler'] | ForEach-Object { "$($_.Kapasite) $($_.Tip)" }) -join ' + ' $avOzet = Get-Safe { ($INV['Güvenlik Yazılımları'] | ForEach-Object { $_.'Ürün' }) -join ', ' } 'YOK' if (-not $avOzet) { $avOzet = 'YOK' } $ozetSatir = [pscustomobject]@{ 'Bilgisayar Adı' = $Bilgisayar 'Cihaz Sınıfı' = $CihazSinifi 'Kullanıcı' = $INV['Sistem'].'Oturum Açan Kullanıcı' 'Üretici' = $INV['Sistem'].'Üretici' 'Model' = $INV['Sistem'].'Model' 'Seri No' = $INV['Sistem'].'Seri No / Servis Etiketi' 'Kalıcı Kimlik' = $CihazKimligi 'Anakart' = $INV['Sistem'].'Anakart' 'Kasa Tipi' = $INV['Sistem'].'Kasa Tipi' 'İşletim Sistemi' = $INV['İşletim Sistemi'].'İşletim Sistemi' 'OS Sürümü' = $INV['İşletim Sistemi'].'Sürüm' 'Destek Durumu' = $DestekDurumu 'Lisans' = $LisansDurumu 'İşlemci' = Get-Safe { $INV['İşlemci'][0].Model } '-' 'RAM' = $INV['Bellek Özeti'].'Toplam RAM' 'RAM Slot' = $INV['Bellek Özeti'].'Slot Kullanımı' 'RAM Tipi' = Get-Safe { $INV['Bellek Modülleri'][0].Tip } '-' 'ECC' = $eccMetin 'Disk' = $diskOzet 'C: Doluluk' = Get-Safe { $sistemDisk.Doluluk } '-' 'BitLocker' = Get-Safe { $sistemDisk.BitLocker } '-' 'IP Adresi' = Get-Safe { $INV['Ağ Adaptörleri'][0].IPv4 } '-' 'MAC Adresi' = Get-Safe { $INV['Ağ Adaptörleri'][0].'MAC Adresi' } '-' 'Antivirüs' = $avOzet 'TPM' = $INV['Güvenlik Özellikleri'].'TPM Mevcut' 'Etki Alanı' = $INV['Sistem'].'Etki Alanı / Çalışma Grubu' 'Çalışma Süresi' = $uptimeM 'Batarya Sağlığı' = Get-Safe { $INV['Batarya'].'Batarya Sağlığı' } '-' 'Kritik Bulgu' = $kritikSayi 'Uyarı' = $uyariSayi 'Envanter Tarihi' = Get-Date -Format 'dd.MM.yyyy HH\:mm' 'Araç Sürümü' = $ScriptSurum # --- Klon / imaj kontrolü sütunları (şema 2 ile eklendi) --------------- # Bu sütunlar bir SONRAKİ cihazın taramasında geri okunur; klon kontrolü # tamamen bunlara dayanır. Yeni sütunlar bilerek en sona eklendi: eski # sütunların sırası hiç değişmediği için v1 ve v2 dosyaları yan yana # konduğunda farkı insan gözüyle görmek kolay kalıyor. 'Şema Sürümü' = $SemaSurumu 'Kimlik Güvenilirliği' = if ($KimlikZayif) { 'Zayıf' } else { 'Donanım' } 'Kurulum Kimliği' = if ($KurulumKimligi) { $KurulumKimligi } else { '' } 'Makine SID Özeti' = if ($MakineSidOzeti) { $MakineSidOzeti } else { '' } 'C: Birim Seri' = if ($CBirimSeri) { $CBirimSeri } else { '' } 'Kurulum Damgası' = if ($KurulumDamgasi) { $KurulumDamgasi } else { '' } 'Klon Durumu' = $KlonDurumu 'Eşleşen Cihaz' = $KlonEslesen 'Güncelleme Yönetimi' = if ($WsusAdres) { "WSUS: $WsusAdres" } else { 'Microsoft Update' } } <# ÖNEMLİ - ayırıcı neden sabit: Önceden -UseCulture kullanılıyordu; bu, ayırıcıyı cihazın diline göre seçiyordu (Türkçe Windows ';', İngilizce Windows ','). Aynı USB ile karışık dilli bir ofiste gezildiğinde tek CSV dosyasına iki farklı ayırıcı yazılıp dosya bozuluyordu. Artık her cihazda ';' kullanılıyor: hem tutarlı, hem Türkçe Excel'de çift tıklamayla doğru açılıyor. #> <# ŞEMA UYUMU - buradaki tuzak sessiz veri kaybıdır: Export-Csv -Append, yazılacak nesnenin sütunları dosyadaki başlıkla uyuşmazsa ya hata verir ya da -Force ile FAZLA SÜTUNLARI SESSİZCE DÜŞÜRÜR. Sahadaki mevcut _TUM-CIHAZLAR.csv dosyaları 1.1 başlığıyla duruyor ve klon sütunları o başlıkta yok. Kontrol edilmeseydi yeni sütunlar hiç yazılmaz, klon kontrolü de "çalışıyor gibi görünüp" hiçbir şey bulamazdı. ÇÖZÜM: eski dosyaya ASLA DOKUNULMAZ. Ne yeniden yazılır, ne kopyalanır, ne silinir. Başlık uyuşmuyorsa yeni satır ayrı bir dosyaya (-v2) yazılır. Kopyalayıp dönüştüren bir göç de denendi; kopyalama başarısız olduğunda akış devam edip birikimli dosyanın üzerine yazabildiği için (300 satır -> 1 satır) terk edildi. Teknisyenin verisi hiçbir senaryoda riske girmez. Beklenen başlık $ozetSatir'ın KENDİSİNDEN türetiliyor; elle sütun listesi tutulmuyor. Elle liste, şema büyüdüğünde güncellenmeyi unutulup sessizce veri kaybettiren klasik hatadır. Klon kontrolü zaten _TUM-CIHAZLAR*.csv kalıbındaki BÜTÜN dosyaları okuduğu için ikiye bölünmüş defter karşılaştırmayı bozmaz. #> function CsvBaslikOku { param([string]$Yol) Get-Safe { $ilk = Get-Content -Path $Yol -TotalCount 1 -Encoding UTF8 -ErrorAction Stop if ([string]::IsNullOrWhiteSpace($ilk)) { return ,@() } ,@(($ilk -split ';') | ForEach-Object { $_.Trim().Trim('"') }) } (,@()) } # Başlıklar Compare-Object ile değil, tek bir imza dizesiyle karşılaştırılıyor. # Compare-Object'in boş dizi verildiğindeki davranışı sürümden sürüme tartışmalı; # birleştirilmiş imza hem sıraya duyarlı hem de hiçbir kenar durumu yok. $beklenenImza = (@($ozetSatir.PSObject.Properties.Name) -join '|') $csvAdaylar = @($csvYol, (Join-Path $CiktiKlasoru '_TUM-CIHAZLAR-v2.csv')) $hedefCsv = $null foreach ($aday in $csvAdaylar) { if (-not (Test-Path $aday)) { $hedefCsv = $aday; break } if (((CsvBaslikOku $aday) -join '|') -eq $beklenenImza) { $hedefCsv = $aday; break } } if (-not $hedefCsv) { $hedefCsv = $csvAdaylar[-1] } $CsvBolundu = ($hedefCsv -ne $csvYol) try { if (Test-Path $hedefCsv) { $ozetSatir | Export-Csv -Path $hedefCsv -NoTypeInformation -Encoding UTF8 -Delimiter ';' -Append -Force } else { $ozetSatir | Export-Csv -Path $hedefCsv -NoTypeInformation -Encoding UTF8 -Delimiter ';' -Force } if ($CsvBolundu) { AdimBitti 'v2 dosyasına yazıldı' DarkYellow } else { AdimBitti } } catch { AdimBitti 'yazılamadı' DarkYellow } if ($CsvBolundu) { Yaz " Mevcut _TUM-CIHAZLAR.csv eski sütun düzeninde; dosyaya dokunulmadı." DarkYellow Yaz " Yeni kayıtlar $(Split-Path $hedefCsv -Leaf) dosyasına yazılıyor (klon kontrolü ikisini de okur)." DarkYellow } # --------------------------------------------------------------------------- # PAYLAŞIM ÖZETİ (WhatsApp / e-posta için düz metin) # --------------------------------------------------------------------------- $oz = New-Object System.Collections.ArrayList [void]$oz.Add("CİHAZ ENVANTERİ - $Bilgisayar") [void]$oz.Add((Get-Date -Format 'dd.MM.yyyy HH\:mm')) [void]$oz.Add('') [void]$oz.Add("Cihaz Sınıfı : $CihazSinifi") [void]$oz.Add("Marka / Model : $($INV['Sistem'].'Üretici') $($INV['Sistem'].'Model')") [void]$oz.Add("Seri No : $($INV['Sistem'].'Seri No / Servis Etiketi')") if ($KimlikEksik) { [void]$oz.Add("Anakart : $($INV['Sistem'].'Anakart')") } [void]$oz.Add("Kullanıcı : $($INV['Sistem'].'Oturum Açan Kullanıcı')") [void]$oz.Add("İşletim Sis. : $($INV['İşletim Sistemi'].'İşletim Sistemi') $($INV['İşletim Sistemi'].'Sürüm') [$DestekDurumu]") [void]$oz.Add("Lisans : $LisansDurumu") [void]$oz.Add("İşlemci : $(Get-Safe { $INV['İşlemci'][0].Model } '-')") [void]$oz.Add("Bellek : $($INV['Bellek Özeti'].'Toplam RAM') ($($INV['Bellek Özeti'].'Slot Kullanımı'))") [void]$oz.Add("Depolama : $diskOzet") [void]$oz.Add("$($env:SystemDrive) Doluluk : $(Get-Safe { $sistemDisk.Doluluk } '-')") [void]$oz.Add("IP Adresi : $(Get-Safe { $INV['Ağ Adaptörleri'][0].IPv4 } '-')") [void]$oz.Add("Antivirüs : $avOzet") if ($INV['Batarya']) { [void]$oz.Add("Batarya : $($INV['Batarya'].'Batarya Sağlığı')") } [void]$oz.Add('') [void]$oz.Add("BULGULAR: $kritikSayi kritik, $uyariSayi uyarı") if ($BulguSirali.Count -eq 0) { [void]$oz.Add(' Dikkat çekici bir bulgu yok.') } else { foreach ($b in ($BulguSirali | Select-Object -First 8)) { [void]$oz.Add((" [{0}] {1}" -f $SeviyeBuyuk[$b.Seviye], $b.Baslik)) } if ($BulguSirali.Count -gt 8) { [void]$oz.Add(" ... ve $($BulguSirali.Count - 8) bulgu daha (tam rapordadır)") } } [void]$oz.Add('') [void]$oz.Add("Tam rapor dosyası: $htmlAdi") $OzetMetin = ($oz -join "`r`n") # --------------------------------------------------------------------------- # ÇIKTI: HTML RAPOR # --------------------------------------------------------------------------- Adim "HTML rapor oluşturuluyor" function HtmlKacis { param([string]$M) if ($null -eq $M) { return '' } return ($M -replace '&','&' -replace '<','<' -replace '>','>' -replace '"','"') } function HtmlTablo { param($Veri, [string]$Baslik, [string]$Id) if ($null -eq $Veri) { return '' } $liste = @($Veri) if ($liste.Count -eq 0) { return "

$(HtmlKacis $Baslik)

Kayıt bulunamadı.

" } $sb = New-Object System.Text.StringBuilder [void]$sb.Append("

$(HtmlKacis $Baslik)

") if ($liste.Count -eq 1 -and $Veri -isnot [array]) { [void]$sb.Append("") foreach ($p in $liste[0].PSObject.Properties) { $deger = HtmlKacis ("$($p.Value)") $sinif = '' # ALL-CAPS degerler bilincli olarak "kotu" isaretidir (KAPALI, ETKİN, YOK...) # "Evet"/"Hayır" baglama gore iyi ya da kotu olabildigi icin renksiz birakilir. # 'AÇIK' bilerek buradadır: otomatik oturum açma "açık" ise bu KÖTÜ bir # durumdur. Aşağıdaki iyi listesindeki '^Açık' kalıbıyla çakışmaz, çünkü # -cmatch büyük/küçük harfe duyarlıdır. if ($deger -cmatch 'KAPALI|Lisanssız|GÜNCEL DEĞİL|^ETKİN$|^EVET - |^YOK$|SORUNLU|^UYARI$|DEVRE DIŞI|^AÇIK$|SORGULANAMADI') { $sinif = " class='kotu'" } elseif ($deger -cmatch '^Açık|Etkinleştirilmiş|^Güncel$|Sağlıklı') { $sinif = " class='iyi'" } [void]$sb.Append("$deger") } [void]$sb.Append("
$(HtmlKacis $p.Name)
") return $sb.ToString() } $sutunlar = $liste[0].PSObject.Properties.Name [void]$sb.Append("
") foreach ($s in $sutunlar) { [void]$sb.Append("") } [void]$sb.Append("") foreach ($satir in $liste) { [void]$sb.Append("") foreach ($s in $sutunlar) { $deger = HtmlKacis ("$($satir.$s)") $sinif = '' if ($deger -cmatch 'KAPALI|GÜNCEL DEĞİL|Lisanssız|SORUNLU|^UYARI$|^ETKİN$') { $sinif = " class='kotu'" } elseif ($deger -cmatch '^Açık|^Güncel$|Sağlıklı|^EVET$|^Bağlı$') { $sinif = " class='iyi'" } [void]$sb.Append("$deger") } [void]$sb.Append("") } [void]$sb.Append("
$(HtmlKacis $s)
") return $sb.ToString() } # --- Bulgular bölümü $bulguHtml = New-Object System.Text.StringBuilder [void]$bulguHtml.Append("

Tespit Edilen Bulgular

") if ($BulguSirali.Count -eq 0) { [void]$bulguHtml.Append("

Otomatik kontrollerde dikkat çekici bir bulgu tespit edilmedi.

") } else { foreach ($b in $BulguSirali) { $sinif = switch ($b.Seviye) { 'Kritik' {'kritik'} 'Uyarı' {'uyari'} default {'bilgi'} } [void]$bulguHtml.Append("
") [void]$bulguHtml.Append("
$(HtmlKacis $b.Seviye)$(HtmlKacis $b.Baslik)
") [void]$bulguHtml.Append("

$(HtmlKacis $b.Detay)

") if ($b.Oneri) { [void]$bulguHtml.Append("

Öneri: $(HtmlKacis $b.Oneri)

") } [void]$bulguHtml.Append("
") } } [void]$bulguHtml.Append("
") # --- Bölümler + yan menü $bolumler = New-Object System.Text.StringBuilder $menu = New-Object System.Text.StringBuilder $sira = 0 foreach ($anahtar in $INV.Keys) { if ($anahtar -eq 'Bulgular') { continue } $sira++ $id = "b$sira" $adet = @($INV[$anahtar]).Count $rozet = if ($INV[$anahtar] -is [array] -and $adet -gt 0) { "$adet" } else { '' } [void]$menu.Append("$(HtmlKacis $anahtar)$rozet") [void]$bolumler.Append((HtmlTablo -Veri $INV[$anahtar] -Baslik $anahtar -Id $id)) } $sureSn = [math]::Round(((Get-Date) - $script:StartTime).TotalSeconds, 1) $cpuKisa = Get-Safe { ($INV['İşlemci'][0].Model -replace '\(R\)|\(TM\)|CPU|Processor','').Trim() } '-' $html = @" Envanter - $Bilgisayar

Cihaz Envanter Raporu

$(HtmlKacis $Bilgisayar)  ·  $(HtmlKacis $INV['Sistem'].'Üretici') $(HtmlKacis $INV['Sistem'].'Model')  ·  $(Get-Date -Format 'dd.MM.yyyy HH\:mm')
Kullanıcı
$(HtmlKacis $INV['Sistem'].'Oturum Açan Kullanıcı')
İşletim Sistemi
$(HtmlKacis $INV['İşletim Sistemi'].'Sürüm')
İşlemci
$(HtmlKacis $cpuKisa)
Bellek
$(HtmlKacis $INV['Bellek Özeti'].'Toplam RAM')
Depolama
$(HtmlKacis $diskOzet)
Windows Lisansı
$(HtmlKacis $LisansDurumu)
Kritik Bulgu
$kritikSayi
Uyarı
$uyariSayi
$($bulguHtml.ToString()) $($bolumler.ToString())
Bu rapor Envanter-Topla.ps1 tarafından otomatik oluşturulmuştur · Tarama süresi: $sureSn sn · Yetki: $(if($Yonetici){'Yönetici'}else{'Standart kullanıcı (kısıtlı veri)'})
"@ Get-Safe { $html | Out-File -FilePath $htmlYol -Encoding UTF8 } AdimBitti # --------------------------------------------------------------------------- # ÖZET EKRAN ÇIKTISI # --------------------------------------------------------------------------- Yaz " ------------------------------------------------------------" DarkGray Yaz "" Yaz " ENVANTER TAMAMLANDI" Green Yaz "" Yaz (" {0,-22}: {1}" -f 'Cihaz', $Bilgisayar) Yaz (" {0,-22}: {1} {2}" -f 'Marka / Model', $INV['Sistem'].'Üretici', $INV['Sistem'].'Model') Yaz (" {0,-22}: {1}" -f 'Seri No', $INV['Sistem'].'Seri No / Servis Etiketi') Yaz (" {0,-22}: {1} {2}" -f 'İşletim Sistemi', $INV['İşletim Sistemi'].'İşletim Sistemi', $INV['İşletim Sistemi'].'Sürüm') Yaz (" {0,-22}: {1}" -f 'İşlemci', (Get-Safe { $INV['İşlemci'][0].Model } '-')) Yaz (" {0,-22}: {1} ({2})" -f 'Bellek', $INV['Bellek Özeti'].'Toplam RAM', $INV['Bellek Özeti'].'Slot Kullanımı') Yaz (" {0,-22}: {1}" -f 'Depolama', $diskOzet) Yaz (" {0,-22}: {1}" -f 'IP Adresi', (Get-Safe { $INV['Ağ Adaptörleri'][0].IPv4 } '-')) Yaz "" if ($kritikSayi -gt 0) { Yaz " Kritik bulgu : $kritikSayi" Red } else { Yaz " Kritik bulgu : 0" DarkGreen } if ($uyariSayi -gt 0) { Yaz " Uyarı : $uyariSayi" Yellow } else { Yaz " Uyarı : 0" DarkGreen } Yaz "" foreach ($b in ($BulguSirali | Select-Object -First 8)) { $r = switch ($b.Seviye) { 'Kritik' {'Red'} 'Uyarı' {'Yellow'} default {'Cyan'} } Yaz (" - [{0}] {1}" -f $SeviyeBuyuk[$b.Seviye], $b.Baslik) $r } if ($BulguSirali.Count -gt 8) { Yaz " ... ve $($BulguSirali.Count - 8) bulgu daha (raporda)" DarkGray } Yaz "" Yaz " Oluşturulan dosyalar:" White Yaz " $htmlYol" DarkCyan Yaz " $jsonYol" DarkCyan Yaz " $csvYol" DarkCyan Yaz "" Yaz " Raporu paylaşmak için soldaki 'Raporu Paylaş' panelini kullanın." DarkGray Yaz " Tarama süresi: $sureSn saniye" DarkGray Yaz "" if (-not $Acma) { Get-Safe { Start-Process $htmlYol } }