- neue Option -DebugDetails - zeigt Pruefung der ChBcPfade, Log-Zugriff, Zeilen-/Trefferanzahlen - zeigt Fallbeispiele aus AppEnforce, wenn keine Anwendung erkannt wird - zeigt fuer jede Anwendung Detailzaehler fuer Regex, Cache, Detection
363 lines
14 KiB
PowerShell
363 lines
14 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Liest den SCCM/MECM-Deployment-Status der letzten Anwendungen auf einem entfernten Client aus.
|
|
.DESCRIPTION
|
|
Das Skript greift ueber die Admin-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.
|
|
Mit -DebugDetails werden zusaetzliche Pruefschritte ausgegeben, damit man
|
|
erkennen kann, wo Log-Zugriff, App-Erkennung oder Regex-Parsing fehlschlaegt.
|
|
.PARAMETER ComputerName
|
|
Name oder IP des Ziel-Clients.
|
|
.PARAMETER CCMPath
|
|
Pfad zu CCM auf dem Ziel (Default C:\Windows\CCM).
|
|
.PARAMETER Last
|
|
Anzahl der letzten Anwendungen, die angezeigt werden sollen (Default: 5).
|
|
.PARAMETER Detailed
|
|
Schaltet zusaetzliche Roh-Logauszuege ein.
|
|
.PARAMETER DebugDetails
|
|
Schaltet Debug-Ausgaben zu Zugriff, Logs, Regex und Auswahl ein.
|
|
.EXAMPLE
|
|
.\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01
|
|
.EXAMPLE
|
|
.\Get-SCCMAppDeployment.ps1 -ComputerName CLIENT01 -Last 10 -Detailed -DebugDetails
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$ComputerName,
|
|
|
|
[string]$CCMPath = "C:\\Windows\\CCM",
|
|
|
|
[int]$Last = 5,
|
|
|
|
[switch]$Detailed,
|
|
|
|
[switch]$DebugDetails
|
|
)
|
|
|
|
#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)'
|
|
}
|
|
|
|
$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)
|
|
$re = '^<!\[LOG\[(?<msg>.*?)\]LOG\]!><time="(?<time>[^"]*)" date="(?<date>[^"]*)" component="(?<comp>[^"]*)" context="[^"]*" type="(?<type>\d)" thread="[^"]*" file="[^"]*">'
|
|
$parsed = @()
|
|
foreach ($line in $Content) {
|
|
if ($line -match $re) {
|
|
$parsed += [PSCustomObject]@{
|
|
Time = "$($Matches.date) $($Matches.time)"
|
|
Component = $Matches.comp
|
|
Type = [int]$Matches.type
|
|
Message = $Matches.msg
|
|
}
|
|
}
|
|
}
|
|
return $parsed
|
|
}
|
|
|
|
function Write-DebugLine {
|
|
param([string]$Message)
|
|
if ($DebugDetails) {
|
|
Write-Host "[DEBUG] $Message" -ForegroundColor DarkYellow
|
|
}
|
|
}
|
|
|
|
function Get-RemoteCCMLog {
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$ComputerName,
|
|
[string]$LogName
|
|
)
|
|
$path = "\\$ComputerName\C$\Windows\CCM\Logs\$LogName"
|
|
Write-DebugLine "Pruefe Log-Pfad: $path"
|
|
|
|
if (-not (Test-Path $path)) {
|
|
Write-Warning "Log nicht vorhanden: $path"
|
|
return @()
|
|
}
|
|
|
|
$content = @()
|
|
try {
|
|
$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 "Konnte $LogName nicht ueber C$ laden: $_"
|
|
try {
|
|
$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 auch im zweiten Versuch nicht lesbar: $_"
|
|
try { $net.RemoveNetworkDrive("Z:", $true, $false) } catch {}
|
|
return @()
|
|
}
|
|
}
|
|
|
|
Write-DebugLine "$LogName geladen: $($content.Count) Zeilen"
|
|
$entries = Parse-SCCMLog -Content $content
|
|
Write-DebugLine "$LogName geparst: $($entries.Count) Eintraege"
|
|
return $entries
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Zugangsdaten erfragen -------------------------------------------------
|
|
|
|
Write-Host "`n=== SCCM Log-Abruf ueber C$ ===" -ForegroundColor Cyan
|
|
|
|
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
|
|
}
|
|
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 = ''
|
|
}
|
|
}
|
|
|
|
$credPassPlain = $credPass
|
|
|
|
#endregion
|
|
|
|
#region Logs laden ------------------------------------------------------------
|
|
|
|
Write-Host "`n=== Lade Logs ===" -ForegroundColor Cyan
|
|
$appAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppDiscovery.log'
|
|
$appEnfAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'AppEnforce.log'
|
|
$casAll = Get-RemoteCCMLog -ComputerName $ComputerName -LogName 'CAS.log'
|
|
|
|
Write-DebugLine "AppDiscovery: $($appAll.Count) Eintraege"
|
|
Write-DebugLine "AppEnforce: $($appEnfAll.Count) Eintraege"
|
|
Write-DebugLine "CAS: $($casAll.Count) Eintraege"
|
|
|
|
#endregion
|
|
|
|
#region Apps ueber Logs ermitteln ---------------------------------------------
|
|
|
|
$appsWithChanges = @()
|
|
$appNames = @{}
|
|
|
|
Write-DebugLine "Suche Application-Namen in AppEnforce..."
|
|
$sampleAppMatches = 0
|
|
foreach ($entry in $appEnfAll) {
|
|
if ($entry.Message -match '(?i)application\s+"([^"]+)"') {
|
|
$sampleAppMatches++
|
|
if ($sampleAppMatches -le 5) {
|
|
Write-DebugLine "Match: $($Matches[1])"
|
|
}
|
|
$name = $Matches[1]
|
|
if (-not $appNames.ContainsKey($name)) {
|
|
$appNames[$name] = $true
|
|
$appsWithChanges += [PSCustomObject]@{
|
|
Name = $name
|
|
LastChange = $entry.Time
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Write-DebugLine "AppEnforce-Matches gesamt: $sampleAppMatches"
|
|
Write-DebugLine "Eindeutige Anwendungsnamen: $($appNames.Count)"
|
|
|
|
if ($appsWithChanges.Count -eq 0) {
|
|
Write-Warning "Keine Anwendungen ueber Regex in AppEnforce gefunden."
|
|
Write-Host "`n=== DEBUG: Fallbeispiele AppEnforce ===" -ForegroundColor Yellow
|
|
$appEnfAll | Select-Object -First 20 | ForEach-Object { Write-Host $_.Message }
|
|
Write-Host "=== Ende DEBUG ===`n" -ForegroundColor Yellow
|
|
}
|
|
|
|
$appsWithChanges = $appsWithChanges | Sort-Object -Property LastChange -Descending
|
|
$selectedApps = $appsWithChanges | Select-Object -First $Last
|
|
|
|
Write-DebugLine "Ausgewaehlte Anwendungen: $($selectedApps.Count)"
|
|
|
|
#endregion
|
|
|
|
#region Auswertung -------------------------------------------------------------
|
|
|
|
$results = foreach ($entry in $selectedApps) {
|
|
$ApplicationName = $entry.Name
|
|
$appEnf = $appEnfAll | Where-Object { $_.Message -like "*$ApplicationName*" }
|
|
$appDisc = $appAll | Where-Object { $_.Message -like "*$ApplicationName*" }
|
|
|
|
Write-DebugLine "Auswerte: $ApplicationName"
|
|
Write-DebugLine " AppEnforce-Zeilen: $($appEnf.Count)"
|
|
Write-DebugLine " AppDiscovery-Zeilen: $($appDisc.Count)"
|
|
|
|
# Content
|
|
$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
|
|
Write-DebugLine " ContentIds gefunden: $($contentIds.Count)"
|
|
|
|
$cachePaths = @()
|
|
foreach ($cid in $contentIds) {
|
|
$casAll | Where-Object { $_.Message -like "*$cid*" -and $_.Message -match 'location = (.+)' } | ForEach-Object {
|
|
if ($_ -match 'location = (.+)') { $cachePaths += $Matches[1].Trim() }
|
|
}
|
|
}
|
|
$cachePaths = $cachePaths | Sort-Object -Unique
|
|
Write-DebugLine " Cache-Pfade gefunden: $($cachePaths.Count)"
|
|
|
|
$arrivedInCache = $cachePaths.Count -gt 0
|
|
$cacheFolder = $cachePaths -join "`n"
|
|
|
|
if (-not $arrivedInCache) {
|
|
$cacheParts = $appEnf | Where-Object { $_.Message -match 'CachePath|CacheResult|Download complete|content download complete' } | Select-Object -ExpandProperty Message
|
|
if ($cacheParts) {
|
|
$cacheFolder = ($cacheParts -join "`n")
|
|
$arrivedInCache = $true
|
|
Write-DebugLine " Cache ueber Keywords erkannt"
|
|
}
|
|
}
|
|
|
|
$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' }
|
|
|
|
$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
|
|
|
|
$reason = ''
|
|
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 = $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)' }
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Ausgabe ----------------------------------------------------------------
|
|
|
|
Write-Host "`n===== SCCM-Deployment-Status: letzte $Last Anwendungen @ $ComputerName =====" -ForegroundColor Cyan
|
|
if ($results) {
|
|
foreach ($r in $results) {
|
|
Write-Host ("`nAnwendung : {0}" -f $r.Application) -ForegroundColor Cyan
|
|
Write-Host ("InstallState : {0}" -f $r.InstallState)
|
|
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 'Erfolg|abgeschlossen|erfolgreich|erkannt') { 'Green' } else { 'Yellow' })
|
|
if ($Detailed) {
|
|
Write-Host ("Fehlerdetails : {0}" -f $r.InstallFehler) -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
Write-Host "Es wurden keine Anwendungen zur Anzeige ausgewaehlt." -ForegroundColor Red
|
|
Write-Host "Hinweis: Starte das Script mit -DebugDetails, um zu sehen, wo es haengt." -ForegroundColor Yellow
|
|
}
|
|
Write-Host ""
|
|
|
|
# Als Objekte zurueckgeben
|
|
$results
|
|
|
|
#endregion |