<# .SYNOPSIS Liest den SCCM/MECM-Deployment-Status der letzten Anwendungen auf einem entfernten Client aus. .DESCRIPTION Das Skript greift ueber dieAdmin-Freigabe C$ auf die CCM-Logs zu und fragt zu Beginn immer nach Benutzername und Passwort. Es zeigt standardmaessig die letzten Anwendungen mit Deployment-Aktivitaet an: - Ob der Content bereits "angekommen" ist (Download abgeschlossen) - In welchem Cache-Ordner der Content liegt - Welche Erkennungsmethode (Detection) greift und deren Ergebnis - Ob die Installation bereits begonnen hat, erfolgreich war oder warum nicht (Fehlercode + begruendende Log-Zeilen) Quellen: - AppDiscovery.log - AppEnforce.log - CAS.log .PARAMETER ComputerName Name oder IP des Ziel-Clients. .PARAMETER CCMPath Pfad zu CCM auf dem Ziel (Default C:\Windows\CCM). .PARAMETER Last Anzahl der letzten Anwendungen, die angezeigt werden sollen (Default: 5). .PARAMETER Detailed Schaltet zusaetzliche Roh-Logauszuege ein. .EXAMPLE .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 .EXAMPLE .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -Last 10 -Detailed #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ComputerName, [string]$CCMPath = "C:\\Windows\\CCM", [int]$Last = 5, [switch]$Detailed ) #region Nachschlagetabellen --------------------------------------------------- $EvalStates = @{ 0 = 'Kein Status (None)'; 1 = 'Verfuegbar (Available)' 2 = 'Uebermittelt (Submitted)'; 3 = 'Erkennung laeuft (Detecting)' 4 = 'Vor-Download (PreDownload)'; 5 = 'Download laeuft (Downloading)' 6 = 'Wartet auf Installation'; 7 = 'Installation laeuft (Installing)' 8 = 'Neustart ausstehend'; 9 = 'Wird ausgefuehrt (Running)' 10 = 'Wiederholung (Retrying)'; 11 = 'Wartet auf Servicefenster' 12 = 'Wartet auf Benutzersitzung'; 13 = 'Wartet auf Benutzer-Abmeldung' 14 = 'Wartet auf Benutzer-Anmeldung'; 15 = 'Wartet (ADU)' 16 = 'Wartet (ADU)'; 17 = 'Wartet auf Orchestrierung' 18 = 'Wartet auf Bereitstellung'; 19 = 'Wartet auf Verbindung' 20 = 'Reserviert'; 21 = 'Wartet (Energie/Modus)' 22 = 'Wartet auf Strom' } $InstallStates = @{ 0 = 'Nicht installiert' 1 = 'Fehler' 2 = 'Erfolgreich' 3 = 'In Bearbeitung' 4 = 'Verfuegbar' 5 = 'Erfolgreich (Neustart noetig)' } $CommonErrors = @{ 0 = 'Erfolg' 1707 = 'Installation erfolgreich' 3010 = 'Erfolg, Neustart erforderlich' 1602 = 'Abbruch durch Benutzer' 1603 = 'Schwerwiegender Installationsfehler' 1618 = 'Andere Installation laeuft bereits (MSI-Sperre)' 1619 = 'MSI-Quellpfad nicht erreichbar' 1633 = 'Plattform wird nicht unterstuetzt' 1635 = 'MSI-Paket beschÃĪdigt/nicht gefunden' } #endregion #region Hilfsfunktionen ------------------------------------------------------- function ConvertTo-EvalStateText { param([int]$Code) if ($EvalStates.ContainsKey($Code)) { return $EvalStates[$Code] } return "Unbekannter Status ($Code)" } function ConvertTo-InstallStateText { param([int]$Code) if ($InstallStates.ContainsKey($Code)) { return $InstallStates[$Code] } return "Unbekannter InstallState ($Code)" } function Get-ErrorText { param([int]$Code) if ($CommonErrors.ContainsKey($Code)) { return $CommonErrors[$Code] } $hex = '0x{0:X8}' -f $Code return "Code $Code ($hex)" } function Parse-SCCMLog { [CmdletBinding()] param([Parameter(Mandatory)][string[]]$Content) $re = '^.*?)\]LOG\]!>[^"]*)" date="(?[^"]*)" component="(?[^"]*)" context="[^"]*" type="(?\d)" thread="[^"]*" file="[^"]*">' foreach ($line in $Content) { if ($line -match $re) { [PSCustomObject]@{ Time = "$($Matches.date) $($Matches.time)" Component = $Matches.comp Type = [int]$Matches.type Message = $Matches.msg } } } } function Get-RemoteCCMLog { [CmdletBinding()] param( [string]$ComputerName, [string]$LogName, [string]$LogRoot, [string]$CredUser, [string]$CredPass ) $path = Join-Path $LogRoot "CCM\Logs\$LogName" if (-not (Test-Path $path)) { Write-Warning "Log nicht vorhanden: $path" return @() } $content = @() try { $net = New-Object -ComObject WScript.Network if ($CredUser -and $CredPass) { $net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false, $CredUser, $CredPass) } else { $net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false) } $content = Get-Content -Path "Z:\Windows\CCM\Logs\$LogName" -Encoding UTF8 -ErrorAction Stop $net.RemoveNetworkDrive("Z:", $true, $false) } catch { Write-Warning "Konnte $LogName nicht ueber C$ laden: $_" try { if ($CredUser -and $CredPass) { $net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false, $CredUser, $CredPass) } else { $net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false) } $content = Get-Content -Path "Z:\Windows\CCM\Logs\$LogName" -Encoding UTF8 -ErrorAction Stop $net.RemoveNetworkDrive("Z:", $true, $false) } catch { Write-Warning "Log $LogName auch im zweiten Versuch nicht lesbar: $_" try { $net.RemoveNetworkDrive("Z:", $true, $false) } catch {} return @() } } $entries = Parse-SCCMLog -Content $content return $entries } #endregion #region Zugangsdaten erfragen ------------------------------------------------- Write-Host "`n=== SCCM Log-Abruf ueber C$ ===" -ForegroundColor Cyan if (-not [string]::IsNullOrWhiteSpace($env:SCCM_USER) -and -not [string]::IsNullOrWhiteSpace($env:SCCM_PASS)) { $credUser = $env:SCCM_USER $credPass = $env:SCCM_PASS Write-Host "Zugangsdaten aus Umgebungsvariablen SCCM_USER / SCCM_PASS verwendet." -ForegroundColor DarkGray } else { $credUser = Read-Host "Benutzername fuer $ComputerName (z.B. DOMAENE\Benutzer oder Benutzer)" $credPass = Read-Host "Passwort fuer $ComputerName" -AsSecureString if ($credPass) { $credPass = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($credPass)) } else { $credPass = '' } } #endregion #region Logs laden ------------------------------------------------------------ $appAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppDiscovery.log' -LogRoot '' -CredUser $credUser -CredPass $credPass $appEnfAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppEnforce.log' -LogRoot '' -CredUser $credUser -CredPass $credPass $casAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'CAS.log' -LogRoot '' -CredUser $credUser -CredPass $credPass #endregion #region Apps ueber Logs ermitteln --------------------------------------------- $appsWithChanges = @() $appNames = @{} foreach ($entry in $appEnfAll) { if ($entry.Message -match '(?i)application\s+"([^"]+)"') { $name = $Matches[1] if (-not $appNames.ContainsKey($name)) { $appNames[$name] = $true $appsWithChanges += [PSCustomObject]@{ Name = $name LastChange = $entry.Time } } } } $appsWithChanges = $appsWithChanges | Sort-Object -Property LastChange -Descending $selectedApps = $appsWithChanges | Select-Object -First $Last #endregion #region Auswertung ------------------------------------------------------------- $results = foreach ($entry in $selectedApps) { $ApplicationName = $entry.Name $appEnf = $appEnfAll | Where-Object { $_.Message -like "*$ApplicationName*" } $appDisc = $appAll | Where-Object { $_.Message -like "*$ApplicationName*" } # Content $contentIds = @() foreach ($e in $appEnf) { if ($e.Message -match 'content(?:ID| with ID)?[ =:]+([A-Fa-f0-9-]{8,})') { $contentIds += $Matches[1] } elseif ($e.Message -match 'ContentId ([A-Fa-f0-9-]{8,})') { $contentIds += $Matches[1] } } $contentIds = $contentIds | Sort-Object -Unique $cachePaths = @() foreach ($cid in $contentIds) { $casAll | Where-Object { $_.Message -like "*$cid*" -and $_.Message -match 'location = (.+)' } | ForEach-Object { if ($_ -match 'location = (.+)') { $cachePaths += $Matches[1].Trim() } } } $cachePaths = $cachePaths | Sort-Object -Unique $arrivedInCache = $cachePaths.Count -gt 0 $cacheFolder = $cachePaths -join "`n" if (-not $arrivedInCache) { $cacheParts = $appEnf | Where-Object { $_.Message -match 'CachePath|CacheResult|Download complete|content download complete' } | Select-Object -ExpandProperty Message if ($cacheParts) { $cacheFolder = ($cacheParts -join "`n") $arrivedInCache = $true } } $detectionLines = $appDisc | Where-Object { $_.Message -match 'detection method|Detected|did not detect|not detected' } | Select-Object -ExpandProperty Message $detectionResult = if ($detectionLines) { if ($detectionLines -match 'did not detect|not detected') { 'NICHT erkannt' } else { 'erkannt / ok' } } else { 'kein Detection-Eintrag gefunden' } $installStarted = ($appEnf | Where-Object { $_.Message -match 'Starting Install enforcement|Starting enforcement' }).Count -gt 0 $installDone = ($appEnf | Where-Object { $_.Message -match 'Enforcement completed|enforcement completed' }).Count -gt 0 $installErrors = $appEnf | Where-Object { $_.Type -in @(2,3) } | Select-Object Time, Message $reason = '' if ($arrivedInCache) { if ($installStarted -and -not $installDone) { $reason = 'Installation laeuft' } elseif ($installDone) { $reason = 'Installation abgeschlossen' } elseif (-not $installStarted) { $reason = 'Noch nicht mit Installation begonnen' } else { $reason = 'Status unklar - Details pruefen' } } else { $reason = 'Content noch nicht im Cache (Download laeuft oder nicht verteilt)' } [PSCustomObject]@{ ComputerName = $ComputerName Application = $ApplicationName InstallState = if ($installDone) { 'Erfolgreich' } elseif ($installStarted) { 'In Bearbeitung' } else { 'nicht ermittelbar' } Angekommen = $arrivedInCache CachePfad = if ($cacheFolder) { $cacheFolder } else { '(kein Cache-Eintrag)' } DetectionErgebnis = $detectionResult InstallStarted = $installStarted InstallAbgeschlossen = $installDone LetzteAenderung = $entry.LastChange Grund = $reason InstallFehler = if ($installErrors) { ($installErrors | ForEach-Object { "$($_.Time): $($_.Message)" }) -join "`n" } else { '(keine)' } } } #endregion #region Ausgabe ---------------------------------------------------------------- Write-Host "`n===== SCCM-Deployment-Status: letzte $Last Anwendungen @ $ComputerName =====" -ForegroundColor Cyan foreach ($r in $results) { Write-Host ("`nAnwendung : {0}" -f $r.Application) -ForegroundColor Cyan Write-Host ("InstallState : {0}" -f $r.InstallState) Write-Host ("Angekommen/Cache : {0}" -f $(if ($r.Angekommen) { 'JA' } else { 'NEIN' })) -ForegroundColor $(if ($r.Angekommen) { 'Green' } else { 'Yellow' }) Write-Host ("Cache-Pfad : {0}" -f $r.CachePfad) Write-Host ("Detection : {0}" -f $r.DetectionErgebnis) Write-Host ("Install gestartet: {0}" -f $(if ($r.InstallStarted) { 'JA' } else { 'NEIN' })) Write-Host ("Install fertig : {0}" -f $(if ($r.InstallAbgeschlossen) { 'JA' } else { 'NEIN' })) Write-Host ("Letzte Aenderung : {0}" -f $r.LetzteAenderung) Write-Host ("Grund/Status : {0}" -f $r.Grund) -ForegroundColor $(if ($r.Grund -match 'Erfolg|abgeschlossen|erfolgreich|erkannt') { 'Green' } else { 'Yellow' }) if ($Detailed) { Write-Host ("Fehlerdetails : {0}" -f $r.InstallFehler) -ForegroundColor DarkGray } } Write-Host "" # Als Objekte zurueckgeben $results #endregion