Powershell/Hermes/Get-SCCMAppDeployment.ps1
buiaca c3f0ee292c feat(Hermes): Get-SCCMAppDeployment.ps1 - SCCM Deployment-Status einer App auslesen
Liest per CIM (CCM_Application) + CCM-Logs (AppDiscovery, AppEnforce, CAS)
den Status einer per Namen angegebenen Anwendung auf einem Client:
- Content angekommen? Cache-Pfad (CacheInfoEx + CAS.log)
- Detection-Methode + Ergebnis
- Installation gestartet/fertig, Fehlercode + Begruendung
Inkl. EvalState-/InstallState-/Fehlercode-Uebersetzungstabellen.
2026-07-28 18:32:26 +02:00

326 lines
13 KiB
PowerShell

<#
.SYNOPSIS
Liest den SCCM/MECM-Deployment-Status einer Anwendung auf einem entfernten Client aus.
.DESCRIPTION
Das Skript verbindet sich mit einem Ziel-Client und liefert zu einer angegebenen
Anwendung folgende Informationen:
- 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 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 Detailed
Schaltet zusaetzliche Roh-Logauszuege ein.
.EXAMPLE
.\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -ApplicationName "Adobe Reader"
.EXAMPLE
.\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -ApplicationName "7-Zip" -Detailed -Credential (Get-Credential)
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ComputerName,
[Parameter(Mandatory)]
[string]$ApplicationName,
[pscredential]$Credential,
[string]$CCMPath = "C:\Windows\CCM",
[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)'
}
# Hauefige SCCM-/MSI-Rueckgabecodes (dezimal); unbekannte werden als Hex angezeigt.
$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[<msg>]LOG]!><time=".." date=".." component=".." context=".." type="<n>" thread=".." file="..">
$re = '^<!\[LOG\[(?<msg>.*?)\]LOG\]!><time="(?<time>[^"]*)" date="(?<date>[^"]*)" component="(?<comp>[^"]*)" context="[^"]*" type="(?<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
# Auf die Anwendung einschraenken (case-insensitive Teilstring)
$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
}
# Admin-Freigabe fuer die Logs mounten (Credential wird hier sauber uebergeben)
$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: App-Status --------------------------------------------------------
$app = $null
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
}
catch {
Write-Warning "CCM_Application-Abfrage fehlgeschlagen: $_"
}
}
#endregion
#region Logs parsen ------------------------------------------------------------
$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 */ }
}
#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' }
}
$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)' }
}
#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 ""
# Als Objekt zurueckgeben (fuer Weiterverarbeitung / Piping)
$result
#endregion
#region Aufraeumen -------------------------------------------------------------
if ($session) { Remove-CimSession $session }
if ($logRoot -and (Get-PSDrive -Name $driveName -ErrorAction SilentlyContinue)) {
Remove-PSDrive -Name $driveName -ErrorAction SilentlyContinue
}
#endregion