From 15fa9f840b559f75c55872b3ca604336d556fd3d Mon Sep 17 00:00:00 2001 From: buiaca Date: Wed, 29 Jul 2026 05:34:51 +0200 Subject: [PATCH] feat(Hermes): Get-SCCMAppDeployment zeigt automatisch letzte Apps + Softwarecenter-Namen - ApplicationName entfernt - statt dessen werden die letzten Applications mit Deployment-Logs ermittelt - Sortierung nach letzter Aenderung, standardmaessig = 5 - Anzeige bevorzugt Anzeigenamen statt interner IDs - Beispiele aktualisiert --- Hermes/Get-SCCMAppDeployment.ps1 | 255 +++++++++++++++++-------------- 1 file changed, 140 insertions(+), 115 deletions(-) diff --git a/Hermes/Get-SCCMAppDeployment.ps1 b/Hermes/Get-SCCMAppDeployment.ps1 index b130543..3be217d 100644 --- a/Hermes/Get-SCCMAppDeployment.ps1 +++ b/Hermes/Get-SCCMAppDeployment.ps1 @@ -1,9 +1,9 @@ <# .SYNOPSIS - Liest den SCCM/MECM-Deployment-Status einer Anwendung auf einem entfernten Client aus. + Liest den SCCM/MECM-Deployment-Status der letzten Anwendungen auf einem entfernten Client aus. .DESCRIPTION - Das Skript verbindet sich mit einem Ziel-Client und liefert zu einer angegebenen - Anwendung folgende Informationen: + 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 @@ -14,18 +14,18 @@ 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 ApplicationName - (Teil-)Name der Anwendung, nach der gesucht wird (case-insensitiver Teilstring). .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 -ApplicationName "Adobe Reader" + .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 .EXAMPLE - .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -ApplicationName "7-Zip" -Detailed -Credential (Get-Credential) + .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -Last 10 -Detailed -Credential (Get-Credential) #> [CmdletBinding()] @@ -33,12 +33,11 @@ param( [Parameter(Mandatory)] [string]$ComputerName, - [Parameter(Mandatory)] - [string]$ApplicationName, - [pscredential]$Credential, - [string]$CCMPath = "C:\Windows\CCM", + [string]$CCMPath = "C:\\Windows\\CCM", + + [int]$Last = 5, [switch]$Detailed ) @@ -69,7 +68,6 @@ $InstallStates = @{ 5 = 'Erfolgreich (Neustart noetig)' } -# Hauefige SCCM-/MSI-Rueckgabecodes (dezimal); unbekannte werden als Hex angezeigt. $CommonErrors = @{ 0 = 'Erfolg' 1707 = 'Installation erfolgreich' @@ -143,7 +141,9 @@ function Get-RemoteCCMLog { return @() } $entries = Parse-SCCMLog -Content $raw - # Auf die Anwendung einschraenken (case-insensitive Teilstring) + if ([string]::IsNullOrWhiteSpace($ApplicationName)) { + return $entries + } $entries | Where-Object { $_.Message -match [regex]::Escape($ApplicationName) } } @@ -162,8 +162,7 @@ catch { $session = $null } -# Admin-Freigabe fuer die Logs mounten (Credential wird hier sauber uebergeben) -$shareUnc = "\\$ComputerName\$(($CCMPath -replace ':','$'))" +$shareUnc = "\\$ComputerName\$($CCMPath -replace ':','$')" $logRoot = $null try { $driveName = "SCCMLOG_$ComputerName" -replace '[^\w]', '' @@ -183,135 +182,161 @@ if (-not $logRoot) { #endregion -#region WMI: App-Status -------------------------------------------------------- +#region WMI: Apps laden und letzte Aenderungen bestimmen ----------------------- -$app = $null +$allAppsWithChanges = @() if ($session) { try { $allApps = Get-CimInstance -CimSession $session -Namespace 'root\ccm\clientsdk' -ClassName CCM_Application -ErrorAction Stop - $app = $allApps | Where-Object { $_.Name -like "*$ApplicationName*" } | Select-Object -First 1 + 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 parsen ------------------------------------------------------------ +#region Logs global laden ----------------------------------------------------- -$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 { @() } -$casAll = if ($logRoot -and (Test-Path (Join-Path $logRoot 'Logs\CAS.log'))) { - Parse-SCCMLog -Content (Get-Content -Path (Join-Path $logRoot "Logs\CAS.log") -Encoding UTF8 -ErrorAction SilentlyContinue) -} else { @() } - -# Content-IDs aus AppEnforce extrahieren, um den Cache-Pfad zuzuordnen -$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 - -# Cache-Pfade aus CAS.log zu diesen Content-IDs -$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 - -# CacheInfoEx ergaenzt (falls WMI verfuegbar) -$cacheEx = @() -if ($session) { - try { - $cacheEx = Get-CimInstance -CimSession $session -Namespace 'root\ccm\SoftMgmtAgent' -ClassName CacheInfoEx -ErrorAction SilentlyContinue | - Where-Object { $contentIds -contains $_.ContentID } - } - catch { /* kein Zugriff - egal */ } +$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 ------------------------------------------------------------- -$arrivedInCache = ($cachePaths.Count -gt 0) -or ($cacheEx.Count -gt 0) -$cacheFolder = if ($cachePaths) { $cachePaths -join "`n" } else { ($cacheEx | ForEach-Object { $_.Location }) -join "`n" } - -# Detection -$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' } - -# Installation -$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 - -# Begruendung, warum (noch) nicht installiert -$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' } +function Get-DisplayNameFromApp($app) { + if ($app.LocalizedDescription) { return $app.LocalizedDescription } + if ($app.Name) { return $app.Name } + return '' } -$result = [PSCustomObject]@{ - ComputerName = $ComputerName - Application = if ($app) { $app.Name } else { "(WMI: keine Uebereinstimmung fuer '$ApplicationName')" } - 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 - Grund = $reason - InstallFehler = if ($installErrors) { ($installErrors | ForEach-Object { "$($_.Time): $($_.Message)" }) -join "`n" } else { '(keine)' } +$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: $ApplicationName @ $ComputerName =====" -ForegroundColor Cyan -Write-Host ("Anwendung : {0}" -f $result.Application) -Write-Host ("InstallState : {0}" -f $result.InstallState) -Write-Host ("EvaluationState : {0}" -f $result.EvaluationState) -if ($result.ErrorCode -ne $null -and $result.ErrorCode -ne 0) { - Write-Host ("Fehler : {0}" -f $result.ErrorText) -ForegroundColor Red -} -Write-Host ("Angekommen/Cache : {0}" -f $(if ($result.Angekommen) { 'JA' } else { 'NEIN' })) -ForegroundColor $(if ($result.Angekommen) { 'Green' } else { 'Yellow' }) -Write-Host ("Cache-Pfad : {0}" -f $result.CachePfad) -Write-Host ("Detection : {0}" -f $result.DetectionErgebnis) -Write-Host ("Install gestartet : {0}" -f $(if ($result.InstallStarted) { 'JA' } else { 'NEIN' })) -Write-Host ("Install fertig : {0}" -f $(if ($result.InstallAbgeschlossen) { 'JA' } else { 'NEIN' })) -Write-Host ("Grund/Status : {0}" -f $result.Grund) -ForegroundColor $(if ($result.Grund -match 'erfolgreich|erkannt') { 'Green' } else { 'Yellow' }) -if ($Detailed) { - Write-Host "`n--- Detection-Logs (AppDiscovery) ---" -ForegroundColor DarkGray - $detectionLines | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - Write-Host "`n--- Install-Fehler (AppEnforce) ---" -ForegroundColor DarkGray - if ($installErrors) { $installErrors | ForEach-Object { Write-Host (" {0}: {1}" -f $_.Time, $_.Message) -ForegroundColor Red } } - else { Write-Host " (keine)" -ForegroundColor Gray } +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 Objekt zurueckgeben (fuer Weiterverarbeitung / Piping) -$result +# Als Objekte zurueckgeben (fuer Weiterverarbeitung / Piping) +$results #endregion @@ -322,4 +347,4 @@ if ($logRoot -and (Get-PSDrive -Name $driveName -ErrorAction SilentlyContinue)) Remove-PSDrive -Name $driveName -ErrorAction SilentlyContinue } -#endregion +#endregion \ No newline at end of file