<# .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]!> $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 # 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