<# .SYNOPSIS Liest den SCCM/MECM-Deployment-Status der letzten Anwendungen auf einem entfernten Client aus. .DESCRIPTION Das Skript verbindet sich mit einem Ziel-Client und zeigt standardmaessig die letzten Anwendungen mit Deployment-Aktivitaet an: - Ob der Content bereits "angekommen" ist (im Cache / 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) Es kombiniert eine WMI/CIM-Abfrage (sauberer Gesamtstatus) mit dem Parsen der relevanten CCM-Logs (AppDiscovery, AppEnforce, CAS) fuer die Details. .PARAMETER ComputerName Name oder IP des Ziel-Clients. Der SCCM-Client muss erreichbar sein (WinRM + Admin-Rechte). .PARAMETER Credential Optional: Credential fuer den Zugriff auf den Client (Admin-Rechte noetig). .PARAMETER CCMPath Optional: 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 -Credential (Get-Credential) #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ComputerName, [pscredential]$Credential, [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) # SCCM-Logformat: ]LOG]!> $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]$ApplicationName ) $path = Join-Path $LogRoot "Logs\$LogName" if (-not (Test-Path $path)) { Write-Warning "Log nicht vorhanden: $path" return @() } try { $raw = Get-Content -Path $path -Encoding UTF8 -ErrorAction Stop } catch { Write-Warning "Log $LogName nicht lesbar: $_" return @() } $entries = Parse-SCCMLog -Content $raw if ([string]::IsNullOrWhiteSpace($ApplicationName)) { return $entries } $entries | Where-Object { $_.Message -match [regex]::Escape($ApplicationName) } } #endregion #region Verbindung aufbauen --------------------------------------------------- $cimParams = @{ ComputerName = $ComputerName; ErrorAction = 'Stop' } if ($Credential) { $cimParams['Credential'] = $Credential } try { $session = New-CimSession @cimParams } catch { Write-Warning "CIM-Sitzung zu $ComputerName fehlgeschlagen (WinRM?): $_" $session = $null } $shareUnc = "\\$ComputerName\$($CCMPath -replace ':','$')" $logRoot = $null try { $driveName = "SCCMLOG_$ComputerName" -replace '[^\w]', '' $drive = New-PSDrive -Name $driveName -PSProvider FileSystem -Root $shareUnc -Credential $Credential -ErrorAction Stop $logRoot = $drive.Root } catch { Write-Warning "Admin-Freigabe $shareUnc nicht per Credential mountbar, versuche aktuellen Kontext: $_" if (Test-Path $shareUnc) { $logRoot = $shareUnc } else { $logRoot = $null } } if (-not $logRoot) { Write-Error "Weder CIM noch Log-Zugriff moeglich. Abbruch." if ($session) { Remove-CimSession $session } return } #endregion #region WMI: Apps laden und letzte Aenderungen bestimmen ----------------------- $allAppsWithChanges = @() if ($session) { try { $allApps = Get-CimInstance -CimSession $session -Namespace 'root\ccm\clientsdk' -ClassName CCM_Application -ErrorAction Stop foreach ($app in $allApps) { $name = $app.Name if ([string]::IsNullOrWhiteSpace($name)) { continue } $appDisc = if ($logRoot) { Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppDiscovery.log' -LogRoot $logRoot -ApplicationName $name } else { @() } $appEnf = if ($logRoot) { Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppEnforce.log' -LogRoot $logRoot -ApplicationName $name } else { @() } $latestLog = ($appEnf + $appDisc | Sort-Object -Property Time -Descending | Select-Object -First 1) $allAppsWithChanges += [PSCustomObject]@{ App = $app Name = $name LastChange = $latestLog.Time LatestMessage = $latestLog.Message } } } catch { Write-Warning "CCM_Application-Abfrage fehlgeschlagen: $_" } } if (-not $allAppsWithChanges) { Write-Warning "Keine Anwendungen mit Deployment-Logs gefunden." } $selectedApps = $allAppsWithChanges | Where-Object { $_.LastChange } | Sort-Object -Property LastChange -Descending | Select-Object -First $Last #endregion #region Logs global laden ----------------------------------------------------- $casAll = @() if ($logRoot -and (Test-Path (Join-Path $logRoot 'Logs\CAS.log'))) { $casAll = Parse-SCCMLog -Content (Get-Content -Path (Join-Path $logRoot "Logs\CAS.log") -Encoding UTF8 -ErrorAction SilentlyContinue) } #endregion #region Auswertung ------------------------------------------------------------- function Get-DisplayNameFromApp($app) { if ($app.LocalizedDescription) { return $app.LocalizedDescription } if ($app.Name) { return $app.Name } return '' } $results = foreach ($entry in $selectedApps) { $app = $entry.App $ApplicationName = $entry.Name $appDisc = if ($logRoot) { Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppDiscovery.log' -LogRoot $logRoot -ApplicationName $ApplicationName } else { @() } $appEnf = if ($logRoot) { Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppEnforce.log' -LogRoot $logRoot -ApplicationName $ApplicationName } else { @() } # 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 -match [regex]::Escape($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 = if ($cachePaths) { $cachePaths -join "`n" } else { '' } 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 ($app) { if ($app.ErrorCode -ne 0) { $reason = "Fehlercode $($app.ErrorCode): $(Get-ErrorText -Code $app.ErrorCode)" } elseif ($app.EvaluationState -in @(11,12,13,14,17,19,21,22)) { $reason = "Wartet: $(ConvertTo-EvalStateText -Code $app.EvaluationState)" } elseif ($app.InstallState -eq 2) { $reason = 'Bereits erfolgreich installiert' } elseif (-not $arrivedInCache) { $reason = 'Content noch nicht im Cache (Download laeuft oder nicht verteilt)' } elseif ($installStarted -and -not $installDone) { $reason = 'Installation laeuft' } elseif (-not $installStarted) { $reason = 'Noch nicht mit Installation begonnen' } else { $reason = 'Status unklar - Details pruefen' } } [PSCustomObject]@{ ComputerName = $ComputerName Application = Get-DisplayNameFromApp $app ApplicationId = $app.Name InstallState = if ($app) { ConvertTo-InstallStateText -Code $app.InstallState } else { 'nicht ermittelbar' } EvaluationState = if ($app) { ConvertTo-EvalStateText -Code $app.EvaluationState } else { 'nicht ermittelbar' } ErrorCode = if ($app) { $app.ErrorCode } else { $null } ErrorText = if ($app -and $app.ErrorCode -ne 0) { Get-ErrorText -Code $app.ErrorCode } else { '' } Angekommen = $arrivedInCache CachePfad = if ($cacheFolder) { $cacheFolder } else { '(kein Cache-Eintrag)' } Detection = if ($detectionLines) { ($detectionLines -join "`n") } else { '(keine Detection-Logs)' } 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 ("ID/Intern : {0}" -f $r.ApplicationId) Write-Host ("InstallState : {0}" -f $r.InstallState) Write-Host ("EvaluationState : {0}" -f $r.EvaluationState) if ($r.ErrorCode -ne $null -and $r.ErrorCode -ne 0) { Write-Host ("Fehler : {0}" -f $r.ErrorText) -ForegroundColor Red } 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 'erfolgreich|erkannt') { 'Green' } else { 'Yellow' }) if ($Detailed) { Write-Host ("Detektion : {0}" -f $r.Detection) -ForegroundColor DarkGray Write-Host ("Fehlerdetails : {0}" -f $r.InstallFehler) -ForegroundColor DarkGray } } Write-Host "" # Als Objekte zurueckgeben (fuer Weiterverarbeitung / Piping) $results #endregion #region Aufraeumen ------------------------------------------------------------- if ($session) { Remove-CimSession $session } if ($logRoot -and (Get-PSDrive -Name $driveName -ErrorAction SilentlyContinue)) { Remove-PSDrive -Name $driveName -ErrorAction SilentlyContinue } #endregion