diff --git a/Hermes/Get-SCCMAppDeployment.ps1 b/Hermes/Get-SCCMAppDeployment.ps1 index 3be217d..90eec4f 100644 --- a/Hermes/Get-SCCMAppDeployment.ps1 +++ b/Hermes/Get-SCCMAppDeployment.ps1 @@ -2,22 +2,24 @@ .SYNOPSIS Liest den SCCM/MECM-Deployment-Status der letzten Anwendungen auf einem entfernten Client aus. .DESCRIPTION - 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) + Das Skript greift ueber dieAdmin-Freigabe C$ auf die CCM-Logs zu und fragt + zu Beginn immer nach Benutzername und Passwort. + + 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 - 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. + Quellen: + - AppDiscovery.log + - AppEnforce.log + - CAS.log .PARAMETER ComputerName - Name oder IP des Ziel-Clients. Der SCCM-Client muss erreichbar sein (WinRM + Admin-Rechte). -.PARAMETER Credential - Optional: Credential fuer den Zugriff auf den Client (Admin-Rechte noetig). + Name oder IP des Ziel-Clients. .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 Anzahl der letzten Anwendungen, die angezeigt werden sollen (Default: 5). .PARAMETER Detailed @@ -25,7 +27,7 @@ .EXAMPLE .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 .EXAMPLE - .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -Last 10 -Detailed -Credential (Get-Credential) + .\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -Last 10 -Detailed #> [CmdletBinding()] @@ -33,8 +35,6 @@ param( [Parameter(Mandatory)] [string]$ComputerName, - [pscredential]$Credential, - [string]$CCMPath = "C:\\Windows\\CCM", [int]$Last = 5, @@ -53,7 +53,7 @@ $EvalStates = @{ 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' + 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' @@ -106,7 +106,6 @@ function Get-ErrorText { 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) { @@ -126,120 +125,111 @@ function Get-RemoteCCMLog { [string]$ComputerName, [string]$LogName, [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)) { Write-Warning "Log nicht vorhanden: $path" return @() } + + $content = @() 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 { - Write-Warning "Log $LogName nicht lesbar: $_" - return @() + Write-Warning "Konnte $LogName nicht ueber C$ laden: $_" + 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)) { - return $entries - } - $entries | Where-Object { $_.Message -match [regex]::Escape($ApplicationName) } + + $entries = Parse-SCCMLog -Content $content + return $entries } #endregion -#region Verbindung aufbauen --------------------------------------------------- +#region Zugangsdaten erfragen ------------------------------------------------- -$cimParams = @{ ComputerName = $ComputerName; ErrorAction = 'Stop' } -if ($Credential) { $cimParams['Credential'] = $Credential } +Write-Host "`n=== SCCM Log-Abruf ueber C$ ===" -ForegroundColor Cyan -try { - $session = New-CimSession @cimParams +if (-not [string]::IsNullOrWhiteSpace($env:SCCM_USER) -and -not [string]::IsNullOrWhiteSpace($env:SCCM_PASS)) { + $credUser = $env:SCCM_USER + $credPass = $env:SCCM_PASS + Write-Host "Zugangsdaten aus Umgebungsvariablen SCCM_USER / SCCM_PASS verwendet." -ForegroundColor DarkGray } -catch { - Write-Warning "CIM-Sitzung zu $ComputerName fehlgeschlagen (WinRM?): $_" - $session = $null -} - -$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 +else { + $credUser = Read-Host "Benutzername fuer $ComputerName (z.B. DOMAENE\Benutzer oder Benutzer)" + $credPass = Read-Host "Passwort fuer $ComputerName" -AsSecureString + if ($credPass) { + $credPass = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($credPass)) + } + else { + $credPass = '' + } } #endregion -#region WMI: Apps laden und letzte Aenderungen bestimmen ----------------------- +#region Logs laden ------------------------------------------------------------ -$allAppsWithChanges = @() -if ($session) { - try { - $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 } +$appAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppDiscovery.log' -LogRoot '' -CredUser $credUser -CredPass $credPass +$appEnfAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppEnforce.log' -LogRoot '' -CredUser $credUser -CredPass $credPass +$casAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'CAS.log' -LogRoot '' -CredUser $credUser -CredPass $credPass - $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) +#endregion - $allAppsWithChanges += [PSCustomObject]@{ - App = $app - Name = $name - LastChange = $latestLog.Time - LatestMessage = $latestLog.Message +#region Apps ueber Logs ermitteln --------------------------------------------- + +$appsWithChanges = @() +$appNames = @{} + +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) { - 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 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) -} +$appsWithChanges = $appsWithChanges | Sort-Object -Property LastChange -Descending +$selectedApps = $appsWithChanges | Select-Object -First $Last #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 { @() } - $appEnf = if ($logRoot) { Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppEnforce.log' -LogRoot $logRoot -ApplicationName $ApplicationName } else { @() } + $appEnf = $appEnfAll | Where-Object { $_.Message -like "*$ApplicationName*" } + $appDisc = $appAll | Where-Object { $_.Message -like "*$ApplicationName*" } # Content $contentIds = @() @@ -251,14 +241,14 @@ $results = foreach ($entry in $selectedApps) { $cachePaths = @() 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() } } } $cachePaths = $cachePaths | Sort-Object -Unique $arrivedInCache = $cachePaths.Count -gt 0 - $cacheFolder = if ($cachePaths) { $cachePaths -join "`n" } else { '' } + $cacheFolder = $cachePaths -join "`n" if (-not $arrivedInCache) { $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 $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' } + if ($arrivedInCache) { + if ($installStarted -and -not $installDone) { $reason = 'Installation laeuft' } + elseif ($installDone) { $reason = 'Installation abgeschlossen' } elseif (-not $installStarted) { $reason = 'Noch nicht mit Installation begonnen' } else { $reason = 'Status unklar - Details pruefen' } + } else { + $reason = 'Content noch nicht im Cache (Download laeuft oder nicht verteilt)' } [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 + ComputerName = $ComputerName + Application = $ApplicationName + InstallState = if ($installDone) { 'Erfolgreich' } elseif ($installStarted) { 'In Bearbeitung' } else { 'nicht ermittelbar' } + Angekommen = $arrivedInCache + CachePfad = if ($cacheFolder) { $cacheFolder } else { '(kein Cache-Eintrag)' } + DetectionErgebnis = $detectionResult + InstallStarted = $installStarted InstallAbgeschlossen = $installDone - LetzteAenderung = $entry.LastChange - Grund = $reason - InstallFehler = if ($installErrors) { ($installErrors | ForEach-Object { "$($_.Time): $($_.Message)" }) -join "`n" } else { '(keine)' } + LetzteAenderung = $entry.LastChange + Grund = $reason + 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 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' }) + Write-Host ("Grund/Status : {0}" -f $r.Grund) -ForegroundColor $(if ($r.Grund -match 'Erfolg|abgeschlossen|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 Objekte zurueckgeben (fuer Weiterverarbeitung / Piping) +# Als Objekte zurueckgeben $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 \ No newline at end of file