# : Startteil fuer die Eingabeaufforderung (cmd) - der eigentliche Code steht weiter unten (PowerShell) @echo off setlocal rem Programme nur aus Windows starten, nie aus dem Ordner dieser Datei (z. B. Downloads) set "NoDefaultCurrentDirectoryInExePath=1" set "PCP_DATEI=%~f0" set "PCP_PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" "%SystemRoot%\System32\net.exe" session >nul 2>&1 if errorlevel 1 ( echo PC-Pflege braucht Administratorrechte. Windows fragt gleich nach. "%PCP_PS%" -NoProfile -Command "Start-Process -FilePath $env:PCP_DATEI -Verb RunAs" exit /b ) "%PCP_PS%" -NoProfile -ExecutionPolicy Bypass -Command "& ([ScriptBlock]::Create([IO.File]::ReadAllText($env:PCP_DATEI, [Text.Encoding]::UTF8)))" exit /b #> # ===================================================================================================== # PC-Pflege für Windows 11 · Version 1.0.0 · https://pcpflege.geofox-it.de # © 2026 Janek Voß · Lizenz: MIT (Text am Ende dieser Datei) · Nutzung auf eigene Gefahr # # Teil 1 prüft den PC (nur lesend). Teil 2 pflegt ihn mit Bordmitteln von Windows. # Das Ergebnis steht in einem Bericht (PDF, erzeugt mit Microsoft Edge; ohne Edge als HTML-Datei), # das Fenster zeigt nur den Fortschritt und die Fragen. # Vor allem, was etwas löscht, installiert oder lange dauert, wird vorher gefragt. # Installiert nichts, legt keine Dienste oder geplanten Aufgaben an, sendet keine Daten. # Ablage: %ProgramData%\PC-Pflege (Berichte, Protokolle, Stand des letzten Laufs, Laufwerks-Messwerte, Sicherungen) # # Start: Doppelklick (Windows fragt nach Administratorrechten) oder Rechtsklick > Als Administrator ausführen. # Hinweis für Bearbeiter: keine typografischen Anführungszeichen im Code (PowerShell hält sie für echte), # im Text »…« verwenden. Datei als UTF-8 ohne BOM mit Windows-Zeilenenden speichern. # ===================================================================================================== $Version = '1.0.0' $ErrorActionPreference = 'SilentlyContinue' try { $Host.UI.RawUI.WindowTitle = "PC-Pflege $Version" } catch {} $ich = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() if (-not $ich.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host 'PC-Pflege braucht Administratorrechte: Rechtsklick auf die Datei > Als Administrator ausführen.' -ForegroundColor Yellow Read-Host 'Enter drücken zum Schließen' | Out-Null return } $Ablage = Join-Path $env:ProgramData 'PC-Pflege' $LogOrdner = Join-Path $Ablage 'Protokolle' $BerichtOrdner = Join-Path $Ablage 'Berichte' $SicherOrdner = Join-Path $Ablage 'Sicherungen' $StandDatei = Join-Path $Ablage 'letzter-lauf.json' $MessDatei = Join-Path $Ablage 'laufwerke-verlauf.csv' # Ablage absichern: Unter ProgramData dürften normale Benutzer sonst Ordner anlegen - etwa eine Verknüpfung (Junction) # auf einen Systemordner, in dem die Pflege mit Administratorrechten dann schreiben oder löschen würde. # Deshalb: Schreiben nur für Administratoren und SYSTEM, alle anderen dürfen lesen (für die Berichte). # Ist die Ablage oder ein Unterordner trotzdem eine Verknüpfung, bricht die PC-Pflege ab. New-Item -ItemType Directory -Force $Ablage | Out-Null $rechte = New-Object Security.AccessControl.DirectorySecurity $rechte.SetAccessRuleProtection($true, $false) foreach ($sid in 'S-1-5-32-544', 'S-1-5-18') { # Administratoren, SYSTEM $rechte.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule((New-Object Security.Principal.SecurityIdentifier $sid), 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow'))) } $rechte.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule((New-Object Security.Principal.SecurityIdentifier 'S-1-5-32-545'), 'ReadAndExecute', 'ContainerInherit, ObjectInherit', 'None', 'Allow'))) # Benutzer: lesen if (-not ((Get-Item -LiteralPath $Ablage -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { Set-Acl -LiteralPath $Ablage -AclObject $rechte } New-Item -ItemType Directory -Force $LogOrdner, $BerichtOrdner | Out-Null foreach ($o in $Ablage, $LogOrdner, $BerichtOrdner, $SicherOrdner) { if ((Test-Path -LiteralPath $o) -and ((Get-Item -LiteralPath $o -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { Write-Host "Abbruch: $o ist eine Verknüpfung auf einen anderen Ordner. Bitte prüfen und den Ordner löschen." -ForegroundColor Red Read-Host 'Enter drücken zum Schließen' | Out-Null return } } $Start = Get-Date $LogDatei = Join-Path $LogOrdner ('PC-Pflege-{0:yyyy-MM-dd_HHmm}.log' -f $Start) $BerichtName = 'PC-Pflege-Bericht-{0:yyyy-MM-dd_HHmm}' -f $Start Start-Transcript -Path $LogDatei | Out-Null $LW = $env:SystemDrive.TrimEnd(':') # Systemlaufwerk, fast immer C # ---------------------------------------------------------------------------------------------------- # Hilfsfunktionen. Während der Pflege landet jede Meldung zusätzlich im Bericht ($Protokoll). # ---------------------------------------------------------------------------------------------------- $Protokoll = New-Object System.Collections.Generic.List[object] $Sammeln = $false function Merke($art, $t) { if ($script:Sammeln) { $script:Protokoll.Add([pscustomobject]@{ Art = $art; Text = "$t" }) } } function Schritt($t) { Write-Host ''; Write-Host "=== $t ===" -ForegroundColor Cyan; Merke 'schritt' $t } function Ok($t) { Write-Host " OK: $t" -ForegroundColor Green; Merke 'ok' $t } function Info($t) { Write-Host " $t"; Merke 'info' $t } function Warn($t) { Write-Host " $t" -ForegroundColor Yellow; Merke 'warn' $t } function Fortschritt($t) { Write-Host " · $t" -ForegroundColor DarkGray } function Frage($q) { $a = Read-Host " $q [j/n]"; $ja = ($a -match '^\s*(j|ja|y|yes)\s*$'); Merke 'frage' ("$q - " + $(if ($ja) { 'ja' } else { 'nein' })); return $ja } function D($d) { if ($d) { ([datetime]$d).ToString('dd.MM.yyyy HH:mm') } else { 'nie' } } function GB($p) { if (-not $p -or -not (Test-Path -LiteralPath $p)) { return 0 } $s = (Get-ChildItem -LiteralPath $p -Recurse -Force -File -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum return [math]::Round($s / 1GB, 2) } # Löschen, ohne Ordner-Verknüpfungen (Junctions, symbolische Links) zu folgen: Remove-Item -Recurse in Windows PowerShell 5.1 # würde in den Zielordner hineinlöschen. Ein normaler Benutzer könnte so in C:\Windows\Temp eine Verknüpfung auf einen # Systemordner legen. Verknüpfungen werden hier nur selbst entfernt; .NET löscht Ordner, ohne Verknüpfungen zu folgen. function SicherLoeschen($e) { try { if ($e.Attributes -band [IO.FileAttributes]::ReparsePoint) { if ($e -is [IO.DirectoryInfo]) { [IO.Directory]::Delete($e.FullName, $false) } else { [IO.File]::Delete($e.FullName) } } elseif ($e -is [IO.DirectoryInfo]) { [IO.Directory]::Delete($e.FullName, $true) } else { if ($e.IsReadOnly) { $e.IsReadOnly = $false }; [IO.File]::Delete($e.FullName) } } catch {} # gesperrte Dateien laufender Programme bleiben einfach liegen } function AlteLoeschen($pfad, $tage) { if (-not (Test-Path -LiteralPath $pfad)) { return } $grenze = (Get-Date).AddDays(-$tage) foreach ($e in @(Get-ChildItem -LiteralPath $pfad -Force -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $grenze })) { SicherLoeschen $e } } function PapierkorbGB { $s = 0 foreach ($v in (Get-Volume | Where-Object { $_.DriveLetter -and $_.DriveType -eq 'Fixed' })) { $s += GB "$($v.DriveLetter):\`$Recycle.Bin" } return $s } function NeustartAussteht { (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') -or (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') } # ---------------------------------------------------------------------------------------------------- # Reste deinstallierter Programme # App-Eintrag : Eintrag in »Installierte Apps«, dessen MSI-Paket nicht mehr registriert ist # oder dessen Deinstallationsprogramm bzw. Programmordner fehlt # Dienst : Windows-Dienst, dessen Programmdatei fehlt # Aufgabe : geplante Aufgabe (außer \Microsoft\), deren Programmdatei fehlt # Verknüpfung : Verknüpfung im Startmenü oder auf dem Desktop, deren Ziel fehlt # Vor dem Entfernen wird jeder Rest in %ProgramData%\PC-Pflege\Sicherungen gesichert. # ---------------------------------------------------------------------------------------------------- function Get-ExePfad([string]$cmd) { if (-not $cmd) { return $null } $c = [Environment]::ExpandEnvironmentVariables($cmd.Trim()) $c = $c -replace '^\\SystemRoot\\', ($env:SystemRoot + '\') -replace '^\\\?\?\\', '' if ($c -match '^"([^"]+)"') { $p = $matches[1] } elseif ($c -match '^(.+?\.(exe|cmd|bat|com))(\s|"|$)') { $p = $matches[1] } else { return $null } if ($p -match '^(system32|syswow64)\\') { $p = Join-Path $env:SystemRoot $p } if (-not [IO.Path]::IsPathRooted($p)) { return $null } return $p } function Get-MsiStatus([string]$guid) { # 5 = installiert, 1 = angekündigt, 2 = fehlt, -1 = unbekannt try { $inst = New-Object -ComObject WindowsInstaller.Installer return [int]$inst.GetType().InvokeMember('ProductState', 'GetProperty', $null, $inst, @($guid)) } catch { return 5 } # im Zweifel als installiert werten } function Find-Reste { $reste = New-Object System.Collections.Generic.List[object] $wurzeln = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' foreach ($w in $wurzeln) { foreach ($key in (Get-ChildItem $w -ErrorAction SilentlyContinue)) { $e = Get-ItemProperty $key.PSPath -ErrorAction SilentlyContinue if (-not $e.DisplayName -or $e.SystemComponent -eq 1 -or $e.ParentKeyName) { continue } $grund = $null $guid = if ($key.PSChildName -match '^\{[0-9A-Fa-f-]{36}\}$') { $key.PSChildName } elseif ($e.UninstallString -match '(\{[0-9A-Fa-f-]{36}\})' -and $e.UninstallString -match 'msiexec') { $matches[1] } if ($e.WindowsInstaller -eq 1 -or ($guid -and $e.UninstallString -match 'msiexec')) { if ($guid -and (Get-MsiStatus $guid) -in -1, 2) { $grund = 'MSI-Paket nicht mehr registriert' } } else { $exe = Get-ExePfad $e.UninstallString $ort = if ($e.InstallLocation) { $e.InstallLocation.Trim().Trim('"') } else { $null } if ($exe -and -not (Test-Path -LiteralPath $exe)) { $grund = 'Deinstallationsprogramm fehlt' } elseif ($ort -and [IO.Path]::IsPathRooted($ort) -and -not (Test-Path -LiteralPath $ort)) { $grund = 'Programmordner fehlt' } } if ($grund) { $reste.Add([pscustomobject]@{ Art = 'App-Eintrag'; Name = "$($e.DisplayName) $($e.DisplayVersion)".Trim(); Grund = $grund; Ziel = $key.Name }) } } } foreach ($s in (Get-CimInstance Win32_Service -ErrorAction SilentlyContinue)) { $exe = Get-ExePfad $s.PathName if ($exe -and -not (Test-Path -LiteralPath $exe)) { $reste.Add([pscustomobject]@{ Art = 'Dienst'; Name = "$($s.DisplayName) ($($s.Name))"; Grund = "Programmdatei fehlt: $exe"; Ziel = $s.Name }) } } foreach ($t in (Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskPath -notlike '\Microsoft\*' })) { foreach ($a in @($t.Actions | Where-Object { $_.Execute })) { $exe = Get-ExePfad ('"' + $a.Execute.Trim('"') + '"') if ($exe -and -not (Test-Path -LiteralPath $exe)) { $reste.Add([pscustomobject]@{ Art = 'Aufgabe'; Name = "$($t.TaskPath)$($t.TaskName)"; Grund = "Programmdatei fehlt: $exe"; Ziel = "$($t.TaskPath)|$($t.TaskName)" }) break } } } $sh = New-Object -ComObject WScript.Shell $orte = "$env:ProgramData\Microsoft\Windows\Start Menu\Programs", "$env:APPDATA\Microsoft\Windows\Start Menu\Programs", [Environment]::GetFolderPath('Desktop'), "$env:PUBLIC\Desktop" foreach ($o in $orte) { foreach ($l in (Get-ChildItem -LiteralPath $o -Filter *.lnk -Recurse -Force -ErrorAction SilentlyContinue)) { try { $ziel = $sh.CreateShortcut($l.FullName).TargetPath } catch { continue } if ($ziel -and [IO.Path]::IsPathRooted($ziel) -and $ziel -notmatch '^\\\\' -and -not (Test-Path -LiteralPath $ziel)) { $reste.Add([pscustomobject]@{ Art = 'Verknüpfung'; Name = $l.FullName; Grund = "Ziel fehlt: $ziel"; Ziel = $l.FullName }) } } } return $reste } function Remove-Rest($r, [string]$sicherung) { New-Item -ItemType Directory -Force $sicherung | Out-Null $datei = Join-Path $sicherung (($r.Art + '_' + $r.Name) -replace '[\\/:*?"<>|\s]+', '_') switch ($r.Art) { 'App-Eintrag' { & reg.exe export $r.Ziel "$datei.reg" /y | Out-Null Remove-Item -LiteralPath ('Registry::' + $r.Ziel) -Recurse -Force -ErrorAction Stop } 'Dienst' { & reg.exe export "HKLM\SYSTEM\CurrentControlSet\Services\$($r.Ziel)" "$datei.reg" /y | Out-Null Stop-Service -Name $r.Ziel -Force -ErrorAction SilentlyContinue & sc.exe delete $r.Ziel | Out-Null if ($LASTEXITCODE -ne 0) { throw "sc delete meldet $LASTEXITCODE" } } 'Aufgabe' { $pfad, $name = $r.Ziel -split '\|', 2 Export-ScheduledTask -TaskPath $pfad -TaskName $name | Set-Content "$datei.xml" -Encoding Unicode Unregister-ScheduledTask -TaskPath $pfad -TaskName $name -Confirm:$false -ErrorAction Stop } 'Verknüpfung' { Copy-Item -LiteralPath $r.Ziel "$datei.lnk" -Force Remove-Item -LiteralPath $r.Ziel -Force -ErrorAction Stop $ordner = Split-Path $r.Ziel if ($ordner -like '*\Start Menu\Programs\*' -and -not (Get-ChildItem -LiteralPath $ordner -Force -ErrorAction SilentlyContinue)) { Remove-Item -LiteralPath $ordner -Force -ErrorAction SilentlyContinue } } } } # ---------------------------------------------------------------------------------------------------- # Geräte-Manager: Bedeutung der häufigsten Fehlercodes # ---------------------------------------------------------------------------------------------------- $GeraeteCodes = @{ 1 = 'nicht richtig eingerichtet'; 3 = 'Treiber beschädigt oder zu wenig Arbeitsspeicher'; 10 = 'kann nicht gestartet werden' 12 = 'nicht genug freie Ressourcen'; 14 = 'braucht einen Neustart'; 18 = 'Treiber neu installieren'; 19 = 'Einstellungen in der Registry beschädigt' 24 = 'nicht richtig erkannt'; 28 = 'kein Treiber installiert'; 31 = 'Treiber konnte nicht geladen werden'; 32 = 'Treiber ist deaktiviert' 39 = 'Treiber fehlt oder ist beschädigt'; 43 = 'Gerät hat einen Fehler gemeldet'; 48 = 'Treiber wurde blockiert'; 52 = 'Signatur des Treibers ungültig' } # ==================================================================================================== # TEIL 1: PRÜFEN (nur lesend) # ==================================================================================================== Clear-Host Write-Host "PC-Pflege $Version" -ForegroundColor Cyan Write-Host 'Teil 1: Der PC wird geprüft. Das dauert einige Minuten, es wird nichts verändert.' Write-Host 'Das Ergebnis erscheint danach als Bericht.' Write-Host '' $letzter = $null if (Test-Path $StandDatei) { $letzter = Get-Content $StandDatei -Raw -Encoding UTF8 | ConvertFrom-Json } # --- Windows --- Fortschritt 'Windows und Updates' $os = Get-CimInstance Win32_OperatingSystem $nt = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' $windows = "$($os.Caption) $($nt.DisplayVersion) (Build $($nt.CurrentBuild).$($nt.UBR))" $pcName = $env:COMPUTERNAME # Letztes kumulatives Update aus dem Windows-Update-Verlauf (Virensignaturen zählen nicht), sonst aus der Hotfix-Liste $letztesUpdate = $null try { $us = (New-Object -ComObject Microsoft.Update.Session).CreateUpdateSearcher() $n = $us.GetTotalHistoryCount() if ($n -gt 0) { $treffer = @($us.QueryHistory(0, [math]::Min($n, 200)) | Where-Object { $_.Operation -eq 1 -and $_.ResultCode -in 2, 3 -and $_.Title -match '(?i)kumulativ|cumulative' } | Sort-Object Date -Descending | Select-Object -First 1) if ($treffer.Count) { $letztesUpdate = $treffer[0].Date.ToLocalTime() } } } catch {} if (-not $letztesUpdate) { $letztesUpdate = (Get-HotFix | Where-Object InstalledOn | Sort-Object InstalledOn -Descending | Select-Object -First 1).InstalledOn } $neustartAus = NeustartAussteht # Reste früherer Windows-Versionen (nach großen Funktionsupdates). In den ersten 10 Tagen braucht Windows sie, # um zur vorherigen Version zurückkehren zu können - erst danach werden sie als überflüssig gemeldet. $altWinOrdner = @("$($LW):\Windows.old", "$($LW):\`$Windows.~BT", "$($LW):\`$Windows.~WS" | Where-Object { Test-Path -LiteralPath $_ }) $altWinGB = 0; foreach ($o in $altWinOrdner) { $altWinGB += GB $o } $altWinAlter = if ($altWinOrdner.Count) { ((Get-Date) - (Get-Item -LiteralPath $altWinOrdner[0] -Force).CreationTime).Days } else { 0 } # --- Speicher --- Fortschritt 'Speicher' $vol = Get-Volume -DriveLetter $LW $freiGB = [math]::Round($vol.SizeRemaining / 1GB, 1) $gesamtGB = [math]::Round($vol.Size / 1GB, 1) $freiPct = if ($vol.Size) { $vol.SizeRemaining / $vol.Size * 100 } else { 100 } $papierkorb = PapierkorbGB $tempGB = (GB $env:TEMP) + (GB "$env:SystemRoot\Temp") $SpPfad = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\StorageSense\Parameters\StoragePolicy' $speicheroptimierung = ((Get-ItemProperty $SpPfad).'01' -eq 1) $andereLW = @(Get-Volume | Where-Object { $_.DriveLetter -and $_.DriveType -eq 'Fixed' -and $_.DriveLetter -ne $LW -and $_.Size -gt 0 }) # Speicherfresser im Benutzerordner: größte Ordner (AppData eine Ebene tiefer) und Dateien ab 1 GB. # Dateien, die nur in OneDrive liegen (nicht heruntergeladen), belegen keinen Platz und zählen nicht mit. Fortschritt 'Speicherfresser im Benutzerordner (kann einige Minuten dauern)' # Ordner-Verknüpfungen (Junctions) werden nicht verfolgt - sonst drohen Endlosschleifen. Ausnahme: OneDrive-Ordner, # die mit »Dateien bei Bedarf« ebenfalls als Verknüpfung markiert sind. $profil = $env:USERPROFILE $oneDrive = @($env:OneDrive, $env:OneDriveConsumer, $env:OneDriveCommercial | Where-Object { $_ } | Sort-Object -Unique) $ordnerGroesse = @{} $grosseDateien = New-Object System.Collections.Generic.List[object] $stapel = New-Object System.Collections.Generic.Stack[IO.DirectoryInfo] $stapel.Push([IO.DirectoryInfo]$profil) while ($stapel.Count) { $ordner = $stapel.Pop() try { $eintraege = $ordner.GetFileSystemInfos() } catch { continue } foreach ($e in $eintraege) { if ($e -is [IO.DirectoryInfo]) { $istLink = [bool]($e.Attributes -band [IO.FileAttributes]::ReparsePoint) if (-not $istLink -or ($oneDrive | Where-Object { $e.FullName -like "$_*" })) { $stapel.Push($e) } continue } if ([int]$e.Attributes -band 0x401000) { continue } # nur online gespeichert (OneDrive) - belegt keinen Platz $teile = $e.FullName.Substring($profil.Length).TrimStart('\').Split('\') $schluessel = if ($teile.Count -lt 2) { '(direkt im Benutzerordner)' } elseif ($teile[0] -eq 'AppData' -and $teile.Count -ge 4) { "AppData\$($teile[1])\$($teile[2])" } else { $teile[0] } $ordnerGroesse[$schluessel] += $e.Length if ($e.Length -ge 1GB) { $grosseDateien.Add([pscustomobject]@{ Pfad = $e.FullName; GB = [math]::Round($e.Length / 1GB, 1); Datum = $e.LastWriteTime }) } } } $topOrdner = @($ordnerGroesse.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 10 | ForEach-Object { [pscustomobject]@{ Ordner = $_.Key; GB = [math]::Round($_.Value / 1GB, 1) } }) $topDateien = @($grosseDateien | Sort-Object GB -Descending | Select-Object -First 10) $downloadsGB = [math]::Round(($ordnerGroesse['Downloads'] + 0) / 1GB, 1) # --- Sicherheit --- Fortschritt 'Sicherheit' $fremdAV = @(Get-CimInstance -Namespace 'root/SecurityCenter2' -ClassName AntiVirusProduct | Where-Object { $_.displayName -notmatch 'Defender' -and (($_.productState -shr 12) -band 0xF) -eq 1 } | ForEach-Object { $_.displayName } | Sort-Object -Unique) $mp = Get-MpComputerStatus $funde = @(Get-MpThreatDetection | Where-Object { $_.InitialDetectionTime -gt $Start.AddDays(-31) }) $pua = (Get-MpPreference).PUAProtection $fwAus = @(Get-NetFirewallProfile | Where-Object { -not $_.Enabled } | ForEach-Object { $_.Name }) $secureBoot = $null; try { $secureBoot = Confirm-SecureBootUEFI -ErrorAction Stop } catch {} $tpm = $null; try { $tpm = Get-Tpm -ErrorAction Stop } catch {} $verschluesselt = $null; try { $verschluesselt = ((Get-BitLockerVolume -MountPoint "$($LW):" -ErrorAction Stop).ProtectionStatus -eq 'On') } catch {} $punkte = @(Get-ComputerRestorePoint | ForEach-Object { [Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) } | Sort-Object) # --- Datensicherung: nur Windows-eigene Wege lassen sich erkennen (andere Sicherungsprogramme nicht) --- Fortschritt 'Datensicherung' $sicherungen = New-Object System.Collections.Generic.List[string] $inOneDrive = @(foreach ($f in @(@('Desktop', 'Desktop'), @('MyDocuments', 'Dokumente'), @('MyPictures', 'Bilder'))) { $pf = [Environment]::GetFolderPath($f[0]) if ($pf -and ($oneDrive | Where-Object { $pf -like "$_\*" })) { $f[1] } }) if ($inOneDrive.Count) { $sicherungen.Add("OneDrive sichert: $($inOneDrive -join ', ')") } if (Test-Path (Join-Path $env:LOCALAPPDATA 'Microsoft\Windows\FileHistory\Configuration\Config1.xml')) { $sicherungen.Add('Dateiversionsverlauf ist eingerichtet') } $wb = @(& wbadmin.exe get versions 2>$null | Where-Object { $_ -match '^(Sicherungszeit|Backup time):\s*(.+)$' } | ForEach-Object { $matches[2].Trim() }) if ($wb.Count) { $sicherungen.Add("Systemabbild-Sicherung, zuletzt $($wb[-1])") } # --- Laufwerke (Messwerte brauchen Adminrechte; jeder Lauf schreibt sie in laufwerke-verlauf.csv) --- Fortschritt 'Laufwerke' $verlauf = @(); if (Test-Path $MessDatei) { $verlauf = @(Import-Csv $MessDatei -Delimiter ';') } $laufwerke = @(Get-PhysicalDisk | Where-Object { $_.BusType -ne 'USB' } | ForEach-Object { $rc = $_ | Get-StorageReliabilityCounter [pscustomobject]@{ Datum = $Start.ToString('yyyy-MM-dd'); Laufwerk = $_.FriendlyName; Art = "$($_.MediaType)"; Zustand = "$($_.HealthStatus)" Verschleiss_Prozent = [int]$rc.Wear; Temperatur_C = [int]$rc.Temperature; Temperatur_Max_C = [int]$rc.TemperatureMax Betriebsstunden = [int]$rc.PowerOnHours; Lesefehler = [int]$rc.ReadErrorsTotal; Schreibfehler = [int]$rc.WriteErrorsTotal } }) $laufwerke | Export-Csv $MessDatei -Delimiter ';' -Append -NoTypeInformation -Encoding UTF8 # --- Geräte mit Treiberproblemen (wie das gelbe Warnzeichen im Geräte-Manager; 22 = absichtlich deaktiviert) --- Fortschritt 'Geräte und Treiber' $geraete = @(Get-CimInstance Win32_PnPEntity | Where-Object { $_.ConfigManagerErrorCode -and $_.ConfigManagerErrorCode -notin 22, 45 } | ForEach-Object { $c = [int]$_.ConfigManagerErrorCode [pscustomobject]@{ Name = $(if ($_.Name) { "$($_.Name)" } else { "$($_.DeviceID)" }); Code = $c; Text = $(if ($GeraeteCodes.ContainsKey($c)) { $GeraeteCodes[$c] } else { "Fehlercode $c" }) } }) # --- Stabilität: Ereignisanzeige der letzten 31 Tage. Immer nach Quelle UND Nummer filtern, # dieselben Nummern kommen auch bei harmlosen Quellen vor (z. B. Kernel-Processor-Power 55). --- Fortschritt 'Stabilität (Ereignisanzeige)' $ab = $Start.AddDays(-31) function Ev($log, $quellen, $ids) { @(foreach ($q in $quellen) { Get-WinEvent -FilterHashtable @{ LogName = $log; ProviderName = $q; Id = $ids; StartTime = $ab } -ErrorAction SilentlyContinue }) } $evBlue = Ev 'System' 'Microsoft-Windows-WER-SystemErrorReporting' 1001 $evAus = Ev 'System' 'Microsoft-Windows-Kernel-Power' 41 if (-not $evAus.Count) { $evAus = Ev 'System' 'EventLog' 6008 } $evDisk = @(Ev 'System' 'disk' (7, 11, 51, 153)) + @(Ev 'System' ('Ntfs', 'Microsoft-Windows-Ntfs') 55) + @(Ev 'System' ('storahci', 'stornvme') 129) $evDiskDetails = @($evDisk | Group-Object ProviderName, Id | ForEach-Object { "$($_.Name): $($_.Count)x" }) $evGrafik = Ev 'System' 'Display' 4101 $evApp = Ev 'Application' 'Application Error' 1000 $abstuerze = @($evApp | ForEach-Object { $_.Properties[0].Value } | Group-Object | Sort-Object Count -Descending | Select-Object -First 5 | ForEach-Object { "$($_.Name) ($($_.Count)x)" }) # --- Autostart und Startdauer --- Fortschritt 'Autostart' function AutostartAn($name, $befehl) { # Task-Manager merkt sich »deaktiviert« unter StartupApproved: erstes Byte gerade = an, ungerade = aus foreach ($w in 'HKCU:', 'HKLM:') { foreach ($u in 'Run', 'Run32', 'StartupFolder') { $werte = Get-ItemProperty "$w\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\$u" -ErrorAction SilentlyContinue foreach ($kandidat in @($name, (Split-Path "$befehl" -Leaf))) { $v = $werte.$kandidat if ($v -is [byte[]] -and $v.Count) { return (($v[0] -band 1) -eq 0) } } } } return $true } $autostartListe = @(Get-CimInstance Win32_StartupCommand | Where-Object { $_.Name } | ForEach-Object { [pscustomobject]@{ Name = "$($_.Name)"; Befehl = "$($_.Command)"; Ort = $(if ($_.Location -match 'Startup') { 'Autostart-Ordner' } elseif ($_.Location -match '^HKLM|HKEY_LOCAL') { 'alle Benutzer' } else { 'dieser Benutzer' }); An = (AutostartAn $_.Name $_.Command) } } | Sort-Object Name -Unique) $autostartAn = @($autostartListe | Where-Object An) $startdauer = $null $evStart = Get-WinEvent -FilterHashtable @{ LogName = 'Microsoft-Windows-Diagnostics-Performance/Operational'; Id = 100 } -MaxEvents 1 -ErrorAction SilentlyContinue if ($evStart) { $bt = ([xml]$evStart.ToXml()).Event.EventData.Data | Where-Object { $_.Name -eq 'BootTime' }; if ($bt) { $startdauer = [math]::Round([int]$bt.'#text' / 1000) } } # --- Akku (nur Laptops) --- $akku = $null $bs = Get-CimInstance -Namespace 'root/wmi' -ClassName BatteryStaticData | Select-Object -First 1 $bf = Get-CimInstance -Namespace 'root/wmi' -ClassName BatteryFullChargedCapacity | Select-Object -First 1 if ($bs.DesignedCapacity -gt 0 -and $bf.FullChargedCapacity -gt 0) { $akku = [math]::Min(100, [math]::Round($bf.FullChargedCapacity / $bs.DesignedCapacity * 100)) } # --- Neu seit dem letzten Lauf: Autostart, geplante Aufgaben, Dienste, Programme --- Fortschritt 'Neues seit dem letzten Lauf und Reste' $autostart = @($autostartListe | ForEach-Object { $_.Name }) $aufgaben = @(Get-ScheduledTask | Where-Object { $_.TaskPath -notlike '\Microsoft\*' -and $_.State -ne 'Disabled' } | ForEach-Object { "$($_.TaskPath)$($_.TaskName)" } | Sort-Object -Unique) $dienste = @(Get-CimInstance Win32_Service | Where-Object { $_.StartMode -eq 'Auto' -and $_.PathName -notmatch '\\Windows\\' } | ForEach-Object { "$($_.DisplayName)" } | Sort-Object -Unique) $k = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' $programme = @(Get-ItemProperty $k | Where-Object { $_.DisplayName -and $_.SystemComponent -ne 1 } | ForEach-Object { "$($_.DisplayName)" } | Sort-Object -Unique) function Neu($alt, $neu) { if ($null -eq $alt) { return @() }; @($neu | Where-Object { $_ -notin @($alt) }) } $neuAutostart = Neu $letzter.Autostart $autostart $neuAufgaben = Neu $letzter.Aufgaben $aufgaben $neuDienste = Neu $letzter.Dienste $dienste $neuProgramme = Neu $letzter.Programme $programme $reste = @(Find-Reste) # ---------------- Bewertung (Ampel): ROT = handeln, GELB = empfohlen ---------------- $befunde = New-Object System.Collections.Generic.List[object] function B($stufe, $bereich, $text, $tun) { $befunde.Add([pscustomobject]@{ Stufe = $stufe; Bereich = $bereich; Text = $text; Tun = $tun }) } # Speicher $freiTxt = '{0}: hat nur noch {1:N0} GB frei ({2:N0} %)' -f $LW, $freiGB, $freiPct if ($freiPct -lt 10) { B 'ROT' 'Speicher' $freiTxt 'Große, nicht mehr gebrauchte Dateien und Programme entfernen – siehe »Speicherfresser«' } elseif ($freiPct -lt 20) { B 'GELB' 'Speicher' $freiTxt 'Große, nicht mehr gebrauchte Dateien und Programme entfernen – siehe »Speicherfresser«' } foreach ($v in $andereLW) { $p = $v.SizeRemaining / $v.Size * 100; if ($p -lt 10) { B 'GELB' 'Speicher' ('{0}: hat nur noch {1:N0} GB frei ({2:N0} %)' -f $v.DriveLetter, ($v.SizeRemaining / 1GB), $p) 'Aufräumen' } } if ($papierkorb -gt 2) { B 'GELB' 'Speicher' ('Papierkorb belegt {0:N1} GB' -f $papierkorb) 'Die Pflege leert ihn (fragt vorher)' } if ($tempGB -gt 2) { B 'GELB' 'Speicher' ('Temporäre Dateien: {0:N1} GB' -f $tempGB) 'Die Pflege räumt sie auf' } if ($downloadsGB -gt 10) { B 'GELB' 'Speicher' ('Der Ordner Downloads belegt {0:N1} GB' -f $downloadsGB) 'Durchsehen: Installationsdateien und Erledigtes löschen' } if (-not $speicheroptimierung) { B 'GELB' 'Speicher' 'Die Speicheroptimierung von Windows ist aus' 'Die Pflege bietet an, sie einzuschalten – dann räumt Windows zwischendurch selbst auf' } if ($altWinOrdner.Count -and $altWinAlter -ge 10) { B 'GELB' 'Speicher' ('Reste einer früheren Windows-Version: {0:N1} GB' -f $altWinGB) 'Die Pflege entfernt sie (fragt vorher)' } # Windows if (-not $letztesUpdate -or $letztesUpdate -lt $Start.AddDays(-45)) { B 'GELB' 'Windows' "Letztes kumulatives Windows-Update: $(D $letztesUpdate)" 'Die Pflege sucht und installiert Updates (fragt vorher)' } if ($neustartAus) { B 'GELB' 'Windows' 'Windows wartet auf einen Neustart, um Updates abzuschließen' 'PC neu starten' } if ($os.LastBootUpTime -lt $Start.AddDays(-14)) { B 'GELB' 'Windows' "Seit $(D $os.LastBootUpTime) nicht neu gestartet" 'PC einmal neu starten (Herunterfahren reicht nicht, wenn der Schnellstart an ist)' } if (-not $punkte.Count) { B 'GELB' 'Windows' 'Es gibt keinen Wiederherstellungspunkt' 'Die Pflege legt einen an und bietet an, den Computerschutz einzuschalten' } if ($autostartAn.Count -gt 15) { B 'GELB' 'Windows' "$($autostartAn.Count) Programme starten mit Windows" 'Liste »Autostart« durchsehen; Unnötiges im Task-Manager > Autostart-Apps deaktivieren' } if ($startdauer -gt 120) { B 'GELB' 'Windows' "Der letzte Start dauerte $startdauer Sekunden" 'Autostart-Programme verringern; bei einer Festplatte (HDD) hilft eine SSD am meisten' } # Datensicherung if (-not $sicherungen.Count) { B 'GELB' 'Datensicherung' 'Keine Datensicherung mit Windows-Bordmitteln gefunden' 'Wichtige Dateien sichern: Startmenü > »Windows-Sicherung« (OneDrive) oder Dateiversionsverlauf auf eine externe Festplatte. Sichert schon ein anderes Programm, kann dieser Punkt ignoriert werden' } # Sicherheit if (-not $fremdAV.Count) { # Ist ein anderer Virenschutz aktiv, ruht Defender absichtlich und wird nicht bemängelt if (-not $mp.RealTimeProtectionEnabled) { B 'ROT' 'Sicherheit' 'Der Echtzeitschutz von Microsoft Defender ist aus' 'Windows-Sicherheit > Viren- & Bedrohungsschutz > Einstellungen verwalten' } if ($mp -and -not $mp.IsTamperProtected) { B 'GELB' 'Sicherheit' 'Der Manipulationsschutz ist aus' 'Windows-Sicherheit > Viren- & Bedrohungsschutz > Einstellungen verwalten > Manipulationsschutz' } if ($funde.Count) { B 'ROT' 'Sicherheit' ('Defender hat im letzten Monat {0} Bedrohung(en) gefunden' -f $funde.Count) 'Windows-Sicherheit > Schutzverlauf ansehen' } if (-not $mp.AntivirusSignatureLastUpdated -or $mp.AntivirusSignatureLastUpdated -lt $Start.AddDays(-3)) { B 'GELB' 'Sicherheit' "Virensignaturen vom $(D $mp.AntivirusSignatureLastUpdated)" 'Die Pflege aktualisiert sie' } if (-not $mp.QuickScanEndTime -or $mp.QuickScanEndTime -lt $Start.AddDays(-35)) { B 'GELB' 'Sicherheit' 'Der letzte Schnellscan ist über 35 Tage her' 'Die Pflege macht einen' } if (-not $mp.FullScanEndTime -or $mp.FullScanEndTime -lt $Start.AddDays(-180)) { B 'GELB' 'Sicherheit' "Letzter vollständiger Virenscan: $(D $mp.FullScanEndTime)" 'Windows-Sicherheit > Viren- & Bedrohungsschutz > Scanoptionen > Vollständige Überprüfung (dauert lange, PC bleibt nutzbar)' } if ($pua -eq 0) { B 'GELB' 'Sicherheit' 'Der Schutz vor potenziell unerwünschten Apps ist aus' 'Windows-Sicherheit > App- & Browsersteuerung > Zuverlässigkeitsbasierter Schutz' } } if ($fwAus.Count) { B 'ROT' 'Sicherheit' "Firewall ist aus für: $($fwAus -join ', ')" 'Windows-Sicherheit > Firewall- & Netzwerkschutz' } if ($secureBoot -eq $false) { B 'GELB' 'Sicherheit' 'Secure Boot ist aus' 'Im UEFI-Setup des PCs einschalten. Vorher die Anleitung des Herstellers lesen' } # Laufwerke foreach ($l in $laufwerke) { if ($l.Zustand -ne 'Healthy') { B 'ROT' 'Laufwerke' "$($l.Laufwerk): Zustand $($l.Zustand)" 'SOFORT wichtige Daten sichern, Laufwerk tauschen lassen' } if ($l.Lesefehler -gt 0 -or $l.Schreibfehler -gt 0) { B 'GELB' 'Laufwerke' "$($l.Laufwerk): $($l.Lesefehler) Lese- und $($l.Schreibfehler) Schreibfehler gezählt" 'Datensicherung prüfen, Laufwerk beobachten' } if ($l.Verschleiss_Prozent -ge 80) { B 'ROT' 'Laufwerke' "$($l.Laufwerk): Verschleiß $($l.Verschleiss_Prozent) %" 'Daten sichern, SSD bald ersetzen' } elseif ($l.Verschleiss_Prozent -ge 50) { B 'GELB' 'Laufwerke' "$($l.Laufwerk): Verschleiß $($l.Verschleiss_Prozent) %" 'Datensicherung sicherstellen' } if ($l.Temperatur_C -gt 70) { B 'ROT' 'Laufwerke' "$($l.Laufwerk): $($l.Temperatur_C) °C" 'Lüftung und Staub prüfen' } elseif ($l.Temperatur_C -gt 60) { B 'GELB' 'Laufwerke' "$($l.Laufwerk): $($l.Temperatur_C) °C" 'Lüftung und Staub prüfen' } } $diskHart = @($evDisk | Where-Object { ($_.ProviderName -eq 'disk' -and $_.Id -in 7, 11) -or $_.Id -eq 55 }) if ($diskHart.Count) { B 'ROT' 'Laufwerke' "Laufwerks- oder Dateisystemfehler: $($evDiskDetails -join ', ')" 'SOFORT wichtige Daten sichern; die Pflege prüft das Dateisystem' } elseif ($evDisk.Count) { B 'GELB' 'Laufwerke' "Ein Laufwerk musste Zugriffe wiederholen: $($evDiskDetails -join ', ')" 'Datensicherung prüfen, Kabel prüfen, beobachten' } # Geräte foreach ($g in $geraete) { B 'GELB' 'Geräte' "$($g.Name): $($g.Text)" 'Windows Update > Erweiterte Optionen > Optionale Updates (Treiber); sonst Geräte-Manager > Gerät > Treiber aktualisieren' } # Stabilität if ($evBlue.Count -ge 3) { B 'ROT' 'Stabilität' "$($evBlue.Count) Bluescreens im letzten Monat" 'Treiber aktualisieren, Arbeitsspeicher testen (Startmenü > Windows-Speicherdiagnose)' } elseif ($evBlue.Count) { B 'GELB' 'Stabilität' "$($evBlue.Count) Bluescreen(s) im letzten Monat" 'Beobachten; bei Wiederholung Treiber aktualisieren' } if ($evAus.Count) { B 'GELB' 'Stabilität' "$($evAus.Count) unerwartete Abschaltung(en) im letzten Monat" 'Falls der PC nicht absichtlich hart ausgeschaltet wurde: Stromversorgung und Temperatur prüfen' } if ($evGrafik.Count -ge 3) { B 'GELB' 'Stabilität' "Der Grafiktreiber hat $($evGrafik.Count)-mal nicht reagiert" 'Grafiktreiber über Windows Update oder beim Hersteller aktualisieren' } # Akku if ($akku -and $akku -lt 60) { B 'GELB' 'Akku' "Der Akku hat noch $akku % seiner ursprünglichen Kapazität" 'Normaler Verschleiß. Reicht die Laufzeit nicht mehr, Akku tauschen lassen' } # Neues und Reste foreach ($a in $neuAutostart) { B 'GELB' 'Neu' "Neuer Autostart-Eintrag: $a" 'Falls unbekannt: Task-Manager > Autostart-Apps > deaktivieren' } if ($reste.Count) { B 'GELB' 'Reste' ('{0} Rest(e) deinstallierter Programme' -f $reste.Count) 'Die Pflege entfernt sie (fragt vorher und sichert sie)' } if (-not $letzter.LetztePflege -or [datetime]$letzter.LetztePflege -lt $Start.AddDays(-35)) { B 'GELB' 'Pflege' $(if ($letzter.LetztePflege) { "Letzte Pflege: $(D $letzter.LetztePflege)" } else { 'Die Pflege lief auf diesem PC noch nie' }) 'Jetzt die Pflege starten' } $rot = @($befunde | Where-Object Stufe -eq 'ROT'); $gelb = @($befunde | Where-Object Stufe -eq 'GELB') function StandSpeichern($pflege) { [ordered]@{ Version = $Version; Stand = $Start.ToString('dd.MM.yyyy HH:mm'); LetztePflege = $pflege Autostart = $autostart; Aufgaben = $aufgaben; Dienste = $dienste; Programme = $programme } | ConvertTo-Json -Depth 3 | Set-Content $StandDatei -Encoding UTF8 } StandSpeichern $letzter.LetztePflege # ==================================================================================================== # BERICHT: HTML-Seite, die Microsoft Edge (gehört zu Windows 11) ohne Fenster als PDF druckt. # Klappt das nicht, öffnet sich die HTML-Seite selbst. Beides liegt in %ProgramData%\PC-Pflege\Berichte. # ==================================================================================================== function Html($s) { [Net.WebUtility]::HtmlEncode("$s") } # nicht »H« nennen: h ist in PowerShell die Abkürzung für Get-History # Tabellenzeilen sammeln: Zeilen $liste { @(Spalte1, Spalte2) } - ergibt immer eine Liste von Zeilen, # auch bei nur einer Zeile (PowerShell würde ein einzelnes Array sonst in seine Zellen auflösen) function Zeilen($objekte, [scriptblock]$spalten) { $z = New-Object System.Collections.Generic.List[object] @($objekte) | Where-Object { $null -ne $_ } | ForEach-Object { $z.Add(@(& $spalten)) } return , $z } function Tabelle($kopf, $zeilen) { if ($zeilen.Count -and $zeilen[0] -isnot [array]) { $zeilen = , $zeilen } $t = '
| $(Html $_) | " }) -join '') + '
|---|
| $(Html $_) | " }) -join '') + '
$(Html $pcName) · $(Html $windows) · $($Start.ToString('dd.MM.yyyy HH:mm')) · Version $Version
| $(Html $b.Bereich) | $(Html $b.Text) → $(Html $b.Tun) |
Nichts – alles im grünen Bereich.
') } if ($mitPflege) { [void]$h.Append('$(Html $e.Text)
") } [void]$h.Append('Keine Autostart-Einträge.
') } [void]$h.Append('Keine – alle Geräte arbeiten ohne Fehlermeldung.
') } [void]$h.Append('Keine gefunden.
') } [void]$h.Append('Erster Lauf auf diesem PC – ab jetzt wird verglichen.
') } else { $neu = New-Object System.Collections.Generic.List[object] foreach ($x in $neuAutostart) { $neu.Add(@('Autostart', $x)) } foreach ($x in $neuDienste) { $neu.Add(@('Dienst', $x)) } foreach ($x in $neuAufgaben) { $neu.Add(@('Geplante Aufgabe', $x)) } foreach ($x in $neuProgramme) { $neu.Add(@('Programm', $x)) } if ($neu.Count) { [void]$h.Append((Tabelle @('Art', 'Name') $neu)) } else { [void]$h.Append("Nichts Neues seit $(Html $letzter.Stand).
") } } [void]$h.Append("