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
This commit is contained in:
buiaca 2026-07-29 05:34:51 +02:00
parent c3f0ee292c
commit 15fa9f840b

View File

@ -1,9 +1,9 @@
<# <#
.SYNOPSIS .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 .DESCRIPTION
Das Skript verbindet sich mit einem Ziel-Client und liefert zu einer angegebenen Das Skript verbindet sich mit einem Ziel-Client und zeigt standardmaessig die letzten
Anwendung folgende Informationen: Anwendungen mit Deployment-Aktivitaet an:
- Ob der Content bereits "angekommen" ist (im Cache / Download abgeschlossen) - Ob der Content bereits "angekommen" ist (im Cache / Download abgeschlossen)
- In welchem Cache-Ordner der Content liegt - In welchem Cache-Ordner der Content liegt
- Welche Erkennungsmethode (Detection) greift und deren Ergebnis - Welche Erkennungsmethode (Detection) greift und deren Ergebnis
@ -14,18 +14,18 @@
relevanten CCM-Logs (AppDiscovery, AppEnforce, CAS) fuer die Details. relevanten CCM-Logs (AppDiscovery, AppEnforce, CAS) fuer die Details.
.PARAMETER ComputerName .PARAMETER ComputerName
Name oder IP des Ziel-Clients. Der SCCM-Client muss erreichbar sein (WinRM + Admin-Rechte). 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 .PARAMETER Credential
Optional: Credential fuer den Zugriff auf den Client (Admin-Rechte noetig). Optional: Credential fuer den Zugriff auf den Client (Admin-Rechte noetig).
.PARAMETER CCMPath .PARAMETER CCMPath
Optional: Pfad zu CCM auf dem Ziel (Default C:\Windows\CCM). 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 .PARAMETER Detailed
Schaltet zusaetzliche Roh-Logauszuege ein. Schaltet zusaetzliche Roh-Logauszuege ein.
.EXAMPLE .EXAMPLE
.\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -ApplicationName "Adobe Reader" .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01
.EXAMPLE .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()] [CmdletBinding()]
@ -33,12 +33,11 @@ param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$ComputerName, [string]$ComputerName,
[Parameter(Mandatory)]
[string]$ApplicationName,
[pscredential]$Credential, [pscredential]$Credential,
[string]$CCMPath = "C:\Windows\CCM", [string]$CCMPath = "C:\\Windows\\CCM",
[int]$Last = 5,
[switch]$Detailed [switch]$Detailed
) )
@ -69,7 +68,6 @@ $InstallStates = @{
5 = 'Erfolgreich (Neustart noetig)' 5 = 'Erfolgreich (Neustart noetig)'
} }
# Hauefige SCCM-/MSI-Rueckgabecodes (dezimal); unbekannte werden als Hex angezeigt.
$CommonErrors = @{ $CommonErrors = @{
0 = 'Erfolg' 0 = 'Erfolg'
1707 = 'Installation erfolgreich' 1707 = 'Installation erfolgreich'
@ -143,7 +141,9 @@ function Get-RemoteCCMLog {
return @() return @()
} }
$entries = Parse-SCCMLog -Content $raw $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) } $entries | Where-Object { $_.Message -match [regex]::Escape($ApplicationName) }
} }
@ -162,8 +162,7 @@ catch {
$session = $null $session = $null
} }
# Admin-Freigabe fuer die Logs mounten (Credential wird hier sauber uebergeben) $shareUnc = "\\$ComputerName\$($CCMPath -replace ':','$')"
$shareUnc = "\\$ComputerName\$(($CCMPath -replace ':','$'))"
$logRoot = $null $logRoot = $null
try { try {
$driveName = "SCCMLOG_$ComputerName" -replace '[^\w]', '' $driveName = "SCCMLOG_$ComputerName" -replace '[^\w]', ''
@ -183,42 +182,73 @@ if (-not $logRoot) {
#endregion #endregion
#region WMI: App-Status -------------------------------------------------------- #region WMI: Apps laden und letzte Aenderungen bestimmen -----------------------
$app = $null $allAppsWithChanges = @()
if ($session) { if ($session) {
try { try {
$allApps = Get-CimInstance -CimSession $session -Namespace 'root\ccm\clientsdk' -ClassName CCM_Application -ErrorAction Stop $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 { catch {
Write-Warning "CCM_Application-Abfrage fehlgeschlagen: $_" 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 #endregion
#region Logs parsen ------------------------------------------------------------ #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 { @() } $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 { @() } $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 # Content
$contentIds = @() $contentIds = @()
foreach ($e in $appEnf) { foreach ($e in $appEnf) {
if ($e.Message -match 'content(?:ID| with ID)?[ =:]+([A-Fa-f0-9\-]{8,})') { if ($e.Message -match 'content(?:ID| with ID)?[ =:]+([A-Fa-f0-9-]{8,})') { $contentIds += $Matches[1] }
$contentIds += $Matches[1] elseif ($e.Message -match 'ContentId ([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 $contentIds = $contentIds | Sort-Object -Unique
# Cache-Pfade aus CAS.log zu diesen Content-IDs
$cachePaths = @() $cachePaths = @()
foreach ($cid in $contentIds) { foreach ($cid in $contentIds) {
$casAll | Where-Object { $_.Message -match [regex]::Escape($cid) -and $_.Message -match 'location = (.+)' } | ForEach-Object { $casAll | Where-Object { $_.Message -match [regex]::Escape($cid) -and $_.Message -match 'location = (.+)' } | ForEach-Object {
@ -227,35 +257,26 @@ foreach ($cid in $contentIds) {
} }
$cachePaths = $cachePaths | Sort-Object -Unique $cachePaths = $cachePaths | Sort-Object -Unique
# CacheInfoEx ergaenzt (falls WMI verfuegbar) $arrivedInCache = $cachePaths.Count -gt 0
$cacheEx = @() $cacheFolder = if ($cachePaths) { $cachePaths -join "`n" } else { '' }
if ($session) {
try { if (-not $arrivedInCache) {
$cacheEx = Get-CimInstance -CimSession $session -Namespace 'root\ccm\SoftMgmtAgent' -ClassName CacheInfoEx -ErrorAction SilentlyContinue | $cacheParts = $appEnf | Where-Object { $_.Message -match 'CachePath|CacheResult|Download complete|content download complete' } | Select-Object -ExpandProperty Message
Where-Object { $contentIds -contains $_.ContentID } if ($cacheParts) {
$cacheFolder = ($cacheParts -join "`n")
$arrivedInCache = $true
} }
catch { /* kein Zugriff - egal */ }
} }
#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 $detectionLines = $appDisc | Where-Object { $_.Message -match 'detection method|Detected|did not detect|not detected' } | Select-Object -ExpandProperty Message
$detectionResult = if ($detectionLines) { $detectionResult = if ($detectionLines) {
if ($detectionLines -match 'did not detect|not detected') { 'NICHT erkannt' } else { 'erkannt / ok' } if ($detectionLines -match 'did not detect|not detected') { 'NICHT erkannt' } else { 'erkannt / ok' }
} else { 'kein Detection-Eintrag gefunden' } } else { 'kein Detection-Eintrag gefunden' }
# Installation
$installStarted = ($appEnf | Where-Object { $_.Message -match 'Starting Install enforcement|Starting enforcement' }).Count -gt 0 $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 $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 $installErrors = $appEnf | Where-Object { $_.Type -in @(2,3) } | Select-Object Time, Message
# Begruendung, warum (noch) nicht installiert
$reason = '' $reason = ''
if ($app) { if ($app) {
if ($app.ErrorCode -ne 0) { $reason = "Fehlercode $($app.ErrorCode): $(Get-ErrorText -Code $app.ErrorCode)" } if ($app.ErrorCode -ne 0) { $reason = "Fehlercode $($app.ErrorCode): $(Get-ErrorText -Code $app.ErrorCode)" }
@ -267,9 +288,10 @@ if ($app) {
else { $reason = 'Status unklar - Details pruefen' } else { $reason = 'Status unklar - Details pruefen' }
} }
$result = [PSCustomObject]@{ [PSCustomObject]@{
ComputerName = $ComputerName ComputerName = $ComputerName
Application = if ($app) { $app.Name } else { "(WMI: keine Uebereinstimmung fuer '$ApplicationName')" } Application = Get-DisplayNameFromApp $app
ApplicationId = $app.Name
InstallState = if ($app) { ConvertTo-InstallStateText -Code $app.InstallState } else { 'nicht ermittelbar' } InstallState = if ($app) { ConvertTo-InstallStateText -Code $app.InstallState } else { 'nicht ermittelbar' }
EvaluationState = if ($app) { ConvertTo-EvalStateText -Code $app.EvaluationState } else { 'nicht ermittelbar' } EvaluationState = if ($app) { ConvertTo-EvalStateText -Code $app.EvaluationState } else { 'nicht ermittelbar' }
ErrorCode = if ($app) { $app.ErrorCode } else { $null } ErrorCode = if ($app) { $app.ErrorCode } else { $null }
@ -280,38 +302,41 @@ $result = [PSCustomObject]@{
DetectionErgebnis = $detectionResult DetectionErgebnis = $detectionResult
InstallStarted = $installStarted InstallStarted = $installStarted
InstallAbgeschlossen = $installDone InstallAbgeschlossen = $installDone
LetzteAenderung = $entry.LastChange
Grund = $reason Grund = $reason
InstallFehler = if ($installErrors) { ($installErrors | ForEach-Object { "$($_.Time): $($_.Message)" }) -join "`n" } else { '(keine)' } InstallFehler = if ($installErrors) { ($installErrors | ForEach-Object { "$($_.Time): $($_.Message)" }) -join "`n" } else { '(keine)' }
} }
}
#endregion #endregion
#region Ausgabe ---------------------------------------------------------------- #region Ausgabe ----------------------------------------------------------------
Write-Host "`n===== SCCM-Deployment-Status: $ApplicationName @ $ComputerName =====" -ForegroundColor Cyan Write-Host "`n===== SCCM-Deployment-Status: letzte $Last Anwendungen @ $ComputerName =====" -ForegroundColor Cyan
Write-Host ("Anwendung : {0}" -f $result.Application) foreach ($r in $results) {
Write-Host ("InstallState : {0}" -f $result.InstallState) Write-Host ("`nAnwendung : {0}" -f $r.Application) -ForegroundColor Cyan
Write-Host ("EvaluationState : {0}" -f $result.EvaluationState) Write-Host ("ID/Intern : {0}" -f $r.ApplicationId)
if ($result.ErrorCode -ne $null -and $result.ErrorCode -ne 0) { Write-Host ("InstallState : {0}" -f $r.InstallState)
Write-Host ("Fehler : {0}" -f $result.ErrorText) -ForegroundColor Red 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 ($result.Angekommen) { 'JA' } else { 'NEIN' })) -ForegroundColor $(if ($result.Angekommen) { 'Green' } else { 'Yellow' }) 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 $result.CachePfad) Write-Host ("Cache-Pfad : {0}" -f $r.CachePfad)
Write-Host ("Detection : {0}" -f $result.DetectionErgebnis) Write-Host ("Detection : {0}" -f $r.DetectionErgebnis)
Write-Host ("Install gestartet : {0}" -f $(if ($result.InstallStarted) { 'JA' } else { 'NEIN' })) Write-Host ("Install gestartet: {0}" -f $(if ($r.InstallStarted) { 'JA' } else { 'NEIN' }))
Write-Host ("Install fertig : {0}" -f $(if ($result.InstallAbgeschlossen) { 'JA' } else { 'NEIN' })) Write-Host ("Install fertig : {0}" -f $(if ($r.InstallAbgeschlossen) { 'JA' } else { 'NEIN' }))
Write-Host ("Grund/Status : {0}" -f $result.Grund) -ForegroundColor $(if ($result.Grund -match 'erfolgreich|erkannt') { 'Green' } else { 'Yellow' }) 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) { if ($Detailed) {
Write-Host "`n--- Detection-Logs (AppDiscovery) ---" -ForegroundColor DarkGray Write-Host ("Detektion : {0}" -f $r.Detection) -ForegroundColor DarkGray
$detectionLines | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } Write-Host ("Fehlerdetails : {0}" -f $r.InstallFehler) -ForegroundColor DarkGray
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 "" Write-Host ""
# Als Objekt zurueckgeben (fuer Weiterverarbeitung / Piping) # Als Objekte zurueckgeben (fuer Weiterverarbeitung / Piping)
$result $results
#endregion #endregion