refactor(Hermes): Get-SCCMAppDeployment auf ChBcLogzugriff umstellen

- statt WinRM/CIM werden Logs ueber Admin-Freigabe C$ gelesen
- bei Ausfuehrung wird immer interaktiv nach Benutzer/Passwort gefragt
- letzte Aenderungen werden aus AppEnforce/AppDiscovery/CAS ermittelt
- zeigt Softwarecenter-Namen/Daten statt internen App-IDs
This commit is contained in:
buiaca 2026-07-29 06:11:07 +02:00
parent 15fa9f840b
commit 36dda3a308

View File

@ -2,22 +2,24 @@
.SYNOPSIS .SYNOPSIS
Liest den SCCM/MECM-Deployment-Status der letzten Anwendungen 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 zeigt standardmaessig die letzten Das Skript greift ueber dieAdmin-Freigabe C$ auf die CCM-Logs zu und fragt
Anwendungen mit Deployment-Aktivitaet an: zu Beginn immer nach Benutzername und Passwort.
- Ob der Content bereits "angekommen" ist (im Cache / Download abgeschlossen)
Es zeigt standardmaessig die letzten Anwendungen mit Deployment-Aktivitaet an:
- Ob der Content bereits "angekommen" ist (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
- Ob die Installation bereits begonnen hat, erfolgreich war oder warum nicht - Ob die Installation bereits begonnen hat, erfolgreich war oder warum nicht
(Fehlercode + begruendende Log-Zeilen) (Fehlercode + begruendende Log-Zeilen)
Es kombiniert eine WMI/CIM-Abfrage (sauberer Gesamtstatus) mit dem Parsen der Quellen:
relevanten CCM-Logs (AppDiscovery, AppEnforce, CAS) fuer die Details. - AppDiscovery.log
- AppEnforce.log
- CAS.log
.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.
.PARAMETER Credential
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). Pfad zu CCM auf dem Ziel (Default C:\Windows\CCM).
.PARAMETER Last .PARAMETER Last
Anzahl der letzten Anwendungen, die angezeigt werden sollen (Default: 5). Anzahl der letzten Anwendungen, die angezeigt werden sollen (Default: 5).
.PARAMETER Detailed .PARAMETER Detailed
@ -25,7 +27,7 @@
.EXAMPLE .EXAMPLE
.\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01
.EXAMPLE .EXAMPLE
.\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -Last 10 -Detailed -Credential (Get-Credential) .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -Last 10 -Detailed
#> #>
[CmdletBinding()] [CmdletBinding()]
@ -33,8 +35,6 @@ param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$ComputerName, [string]$ComputerName,
[pscredential]$Credential,
[string]$CCMPath = "C:\\Windows\\CCM", [string]$CCMPath = "C:\\Windows\\CCM",
[int]$Last = 5, [int]$Last = 5,
@ -53,7 +53,7 @@ $EvalStates = @{
10 = 'Wiederholung (Retrying)'; 11 = 'Wartet auf Servicefenster' 10 = 'Wiederholung (Retrying)'; 11 = 'Wartet auf Servicefenster'
12 = 'Wartet auf Benutzersitzung'; 13 = 'Wartet auf Benutzer-Abmeldung' 12 = 'Wartet auf Benutzersitzung'; 13 = 'Wartet auf Benutzer-Abmeldung'
14 = 'Wartet auf Benutzer-Anmeldung'; 15 = 'Wartet (ADU)' 14 = 'Wartet auf Benutzer-Anmeldung'; 15 = 'Wartet (ADU)'
16 = 'Wartet (ADU)'; 17 = 'Wartet auf Orchestrierung' 16 = 'Wartet (ADU)'; 17 = 'Wartet auf Orchestrierung'
18 = 'Wartet auf Bereitstellung'; 19 = 'Wartet auf Verbindung' 18 = 'Wartet auf Bereitstellung'; 19 = 'Wartet auf Verbindung'
20 = 'Reserviert'; 21 = 'Wartet (Energie/Modus)' 20 = 'Reserviert'; 21 = 'Wartet (Energie/Modus)'
22 = 'Wartet auf Strom' 22 = 'Wartet auf Strom'
@ -106,7 +106,6 @@ function Get-ErrorText {
function Parse-SCCMLog { function Parse-SCCMLog {
[CmdletBinding()] [CmdletBinding()]
param([Parameter(Mandatory)][string[]]$Content) 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="[^"]*">' $re = '^<!\[LOG\[(?<msg>.*?)\]LOG\]!><time="(?<time>[^"]*)" date="(?<date>[^"]*)" component="(?<comp>[^"]*)" context="[^"]*" type="(?<type>\d)" thread="[^"]*" file="[^"]*">'
foreach ($line in $Content) { foreach ($line in $Content) {
if ($line -match $re) { if ($line -match $re) {
@ -126,120 +125,111 @@ function Get-RemoteCCMLog {
[string]$ComputerName, [string]$ComputerName,
[string]$LogName, [string]$LogName,
[string]$LogRoot, [string]$LogRoot,
[string]$ApplicationName [string]$CredUser,
[string]$CredPass
) )
$path = Join-Path $LogRoot "Logs\$LogName" $path = Join-Path $LogRoot "CCM\Logs\$LogName"
if (-not (Test-Path $path)) { if (-not (Test-Path $path)) {
Write-Warning "Log nicht vorhanden: $path" Write-Warning "Log nicht vorhanden: $path"
return @() return @()
} }
$content = @()
try { try {
$raw = Get-Content -Path $path -Encoding UTF8 -ErrorAction Stop $net = New-Object -ComObject WScript.Network
if ($CredUser -and $CredPass) {
$net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false, $CredUser, $CredPass)
}
else {
$net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false)
}
$content = Get-Content -Path "Z:\Windows\CCM\Logs\$LogName" -Encoding UTF8 -ErrorAction Stop
$net.RemoveNetworkDrive("Z:", $true, $false)
} }
catch { catch {
Write-Warning "Log $LogName nicht lesbar: $_" Write-Warning "Konnte $LogName nicht ueber C$ laden: $_"
return @() try {
if ($CredUser -and $CredPass) {
$net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false, $CredUser, $CredPass)
}
else {
$net.MapNetworkDrive("Z:", "\\$ComputerName\C$", $false)
}
$content = Get-Content -Path "Z:\Windows\CCM\Logs\$LogName" -Encoding UTF8 -ErrorAction Stop
$net.RemoveNetworkDrive("Z:", $true, $false)
}
catch {
Write-Warning "Log $LogName auch im zweiten Versuch nicht lesbar: $_"
try { $net.RemoveNetworkDrive("Z:", $true, $false) } catch {}
return @()
}
} }
$entries = Parse-SCCMLog -Content $raw
if ([string]::IsNullOrWhiteSpace($ApplicationName)) { $entries = Parse-SCCMLog -Content $content
return $entries return $entries
}
$entries | Where-Object { $_.Message -match [regex]::Escape($ApplicationName) }
} }
#endregion #endregion
#region Verbindung aufbauen --------------------------------------------------- #region Zugangsdaten erfragen -------------------------------------------------
$cimParams = @{ ComputerName = $ComputerName; ErrorAction = 'Stop' } Write-Host "`n=== SCCM Log-Abruf ueber C$ ===" -ForegroundColor Cyan
if ($Credential) { $cimParams['Credential'] = $Credential }
try { if (-not [string]::IsNullOrWhiteSpace($env:SCCM_USER) -and -not [string]::IsNullOrWhiteSpace($env:SCCM_PASS)) {
$session = New-CimSession @cimParams $credUser = $env:SCCM_USER
$credPass = $env:SCCM_PASS
Write-Host "Zugangsdaten aus Umgebungsvariablen SCCM_USER / SCCM_PASS verwendet." -ForegroundColor DarkGray
} }
catch { else {
Write-Warning "CIM-Sitzung zu $ComputerName fehlgeschlagen (WinRM?): $_" $credUser = Read-Host "Benutzername fuer $ComputerName (z.B. DOMAENE\Benutzer oder Benutzer)"
$session = $null $credPass = Read-Host "Passwort fuer $ComputerName" -AsSecureString
} if ($credPass) {
$credPass = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($credPass))
$shareUnc = "\\$ComputerName\$($CCMPath -replace ':','$')" }
$logRoot = $null else {
try { $credPass = ''
$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 #endregion
#region WMI: Apps laden und letzte Aenderungen bestimmen ----------------------- #region Logs laden ------------------------------------------------------------
$allAppsWithChanges = @() $appAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppDiscovery.log' -LogRoot '' -CredUser $credUser -CredPass $credPass
if ($session) { $appEnfAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppEnforce.log' -LogRoot '' -CredUser $credUser -CredPass $credPass
try { $casAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'CAS.log' -LogRoot '' -CredUser $credUser -CredPass $credPass
$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 { @() } #endregion
$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]@{ #region Apps ueber Logs ermitteln ---------------------------------------------
App = $app
Name = $name $appsWithChanges = @()
LastChange = $latestLog.Time $appNames = @{}
LatestMessage = $latestLog.Message
foreach ($entry in $appEnfAll) {
if ($entry.Message -match '(?i)application\s+"([^"]+)"') {
$name = $Matches[1]
if (-not $appNames.ContainsKey($name)) {
$appNames[$name] = $true
$appsWithChanges += [PSCustomObject]@{
Name = $name
LastChange = $entry.Time
} }
} }
} }
catch {
Write-Warning "CCM_Application-Abfrage fehlgeschlagen: $_"
}
} }
if (-not $allAppsWithChanges) { $appsWithChanges = $appsWithChanges | Sort-Object -Property LastChange -Descending
Write-Warning "Keine Anwendungen mit Deployment-Logs gefunden." $selectedApps = $appsWithChanges | Select-Object -First $Last
}
$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 #endregion
#region Auswertung ------------------------------------------------------------- #region Auswertung -------------------------------------------------------------
function Get-DisplayNameFromApp($app) {
if ($app.LocalizedDescription) { return $app.LocalizedDescription }
if ($app.Name) { return $app.Name }
return ''
}
$results = foreach ($entry in $selectedApps) { $results = foreach ($entry in $selectedApps) {
$app = $entry.App
$ApplicationName = $entry.Name $ApplicationName = $entry.Name
$appEnf = $appEnfAll | Where-Object { $_.Message -like "*$ApplicationName*" }
$appDisc = if ($logRoot) { Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppDiscovery.log' -LogRoot $logRoot -ApplicationName $ApplicationName } else { @() } $appDisc = $appAll | Where-Object { $_.Message -like "*$ApplicationName*" }
$appEnf = if ($logRoot) { Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppEnforce.log' -LogRoot $logRoot -ApplicationName $ApplicationName } else { @() }
# Content # Content
$contentIds = @() $contentIds = @()
@ -251,14 +241,14 @@ $results = foreach ($entry in $selectedApps) {
$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 -like "*$cid*" -and $_.Message -match 'location = (.+)' } | ForEach-Object {
if ($_ -match 'location = (.+)') { $cachePaths += $Matches[1].Trim() } if ($_ -match 'location = (.+)') { $cachePaths += $Matches[1].Trim() }
} }
} }
$cachePaths = $cachePaths | Sort-Object -Unique $cachePaths = $cachePaths | Sort-Object -Unique
$arrivedInCache = $cachePaths.Count -gt 0 $arrivedInCache = $cachePaths.Count -gt 0
$cacheFolder = if ($cachePaths) { $cachePaths -join "`n" } else { '' } $cacheFolder = $cachePaths -join "`n"
if (-not $arrivedInCache) { if (-not $arrivedInCache) {
$cacheParts = $appEnf | Where-Object { $_.Message -match 'CachePath|CacheResult|Download complete|content download complete' } | Select-Object -ExpandProperty Message $cacheParts = $appEnf | Where-Object { $_.Message -match 'CachePath|CacheResult|Download complete|content download complete' } | Select-Object -ExpandProperty Message
@ -278,33 +268,27 @@ $results = foreach ($entry in $selectedApps) {
$installErrors = $appEnf | Where-Object { $_.Type -in @(2,3) } | Select-Object Time, Message $installErrors = $appEnf | Where-Object { $_.Type -in @(2,3) } | Select-Object Time, Message
$reason = '' $reason = ''
if ($app) { if ($arrivedInCache) {
if ($app.ErrorCode -ne 0) { $reason = "Fehlercode $($app.ErrorCode): $(Get-ErrorText -Code $app.ErrorCode)" } if ($installStarted -and -not $installDone) { $reason = 'Installation laeuft' }
elseif ($app.EvaluationState -in @(11,12,13,14,17,19,21,22)) { $reason = "Wartet: $(ConvertTo-EvalStateText -Code $app.EvaluationState)" } elseif ($installDone) { $reason = 'Installation abgeschlossen' }
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' } elseif (-not $installStarted) { $reason = 'Noch nicht mit Installation begonnen' }
else { $reason = 'Status unklar - Details pruefen' } else { $reason = 'Status unklar - Details pruefen' }
} else {
$reason = 'Content noch nicht im Cache (Download laeuft oder nicht verteilt)'
} }
[PSCustomObject]@{ [PSCustomObject]@{
ComputerName = $ComputerName ComputerName = $ComputerName
Application = Get-DisplayNameFromApp $app Application = $ApplicationName
ApplicationId = $app.Name InstallState = if ($installDone) { 'Erfolgreich' } elseif ($installStarted) { 'In Bearbeitung' } else { 'nicht ermittelbar' }
InstallState = if ($app) { ConvertTo-InstallStateText -Code $app.InstallState } else { 'nicht ermittelbar' } Angekommen = $arrivedInCache
EvaluationState = if ($app) { ConvertTo-EvalStateText -Code $app.EvaluationState } else { 'nicht ermittelbar' } CachePfad = if ($cacheFolder) { $cacheFolder } else { '(kein Cache-Eintrag)' }
ErrorCode = if ($app) { $app.ErrorCode } else { $null } DetectionErgebnis = $detectionResult
ErrorText = if ($app -and $app.ErrorCode -ne 0) { Get-ErrorText -Code $app.ErrorCode } else { '' } InstallStarted = $installStarted
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 InstallAbgeschlossen = $installDone
LetzteAenderung = $entry.LastChange 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)' }
} }
} }
@ -315,36 +299,21 @@ $results = foreach ($entry in $selectedApps) {
Write-Host "`n===== SCCM-Deployment-Status: letzte $Last Anwendungen @ $ComputerName =====" -ForegroundColor Cyan Write-Host "`n===== SCCM-Deployment-Status: letzte $Last Anwendungen @ $ComputerName =====" -ForegroundColor Cyan
foreach ($r in $results) { foreach ($r in $results) {
Write-Host ("`nAnwendung : {0}" -f $r.Application) -ForegroundColor Cyan 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 ("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 ("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 ("Cache-Pfad : {0}" -f $r.CachePfad)
Write-Host ("Detection : {0}" -f $r.DetectionErgebnis) Write-Host ("Detection : {0}" -f $r.DetectionErgebnis)
Write-Host ("Install gestartet: {0}" -f $(if ($r.InstallStarted) { 'JA' } else { 'NEIN' })) 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 ("Install fertig : {0}" -f $(if ($r.InstallAbgeschlossen) { 'JA' } else { 'NEIN' }))
Write-Host ("Letzte Aenderung : {0}" -f $r.LetzteAenderung) 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' }) Write-Host ("Grund/Status : {0}" -f $r.Grund) -ForegroundColor $(if ($r.Grund -match 'Erfolg|abgeschlossen|erfolgreich|erkannt') { 'Green' } else { 'Yellow' })
if ($Detailed) { if ($Detailed) {
Write-Host ("Detektion : {0}" -f $r.Detection) -ForegroundColor DarkGray
Write-Host ("Fehlerdetails : {0}" -f $r.InstallFehler) -ForegroundColor DarkGray Write-Host ("Fehlerdetails : {0}" -f $r.InstallFehler) -ForegroundColor DarkGray
} }
} }
Write-Host "" Write-Host ""
# Als Objekte zurueckgeben (fuer Weiterverarbeitung / Piping) # Als Objekte zurueckgeben
$results $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 #endregion