Inside Exchange Online PowerShell
Auf dieser Seite sammle ich ein paar PowerShell-Aufrufe und Erkenntnisse, die ich immer mal wieder brauchen kenn. Sie Analyse wurde mit der Exchange Online PowerShell 3.3.0 Anfang Dezember 2023 durchgeführt. Neuere Versionen können sich anders verhalten.
Auslöser
Eigentlich wollte ich nur besser verstehen, wie so ein Powershell-Modul arbeitet und hinter die Kulissen schauen. Wir wissen alle, wie so eine Exchange Online PowerShell Sessions ablaufen kann:
# PowerShell starten # Mit Exchange Online verbinden PS C:> Connect-ExchangeOnline # und dann die gewünschten Aktionen ausführen PS C:> Get-Mailbox PS C:> (Get-OrganizationConfig).identity msxfaqdev.onmicrosoft.com # An Ende wieder trennen PS C:> Disconnect-ExchangeOnline
Damit all die Befehle zwischen dem "Connect" und dem "Disconnect" funktionieren, muss die PowerShell irgendwo die Authentifizierung speichern und mit einem einfachen "Get-Variable" habe ich keine Session-Variable o.ä. gefunden. Daher muss die PowerShell das irgendwie anders machen.
Tipp: Allein durch das Lesen und Analysieren von Microsoft PowerShell-Modulen können Sie viel über "richtiges Programmieren" lernen und sich einige Tricks und Kniffe abschauen.
Analyse Disconnect-ExchangeOnline
Ich habe daher eine Verbindung zu meinem Developer Tenant aufgebaut und dann den Code hinter Disconnect-ExchangeOnline angeschaut. Das geht recht einfach mit:
(get-command Disconnect-ExchangeOnline).scriptblock
Die PowerShell liefert dann den kompletten Code, der bei einem Disconnect-ExchangeOnline ausgeführt wird. Ich musste gar nicht den ganzen Code verstehen aber die Stelle zum "Löschen" der Authentifizierungsdaten ist einfach auszumachen. Hier ein Auszug:
# Import the module once more to ensure that Test-ActiveToken is present $ExoPowershellModule = "Microsoft.Exchange.Management.ExoPowershellGalleryModule.dll"; $ModulePath = [System.IO.Path]::Combine($PSScriptRoot, $ExoPowershellModule); Import-Module $ModulePath -Cmdlet Clear-ActiveToken; $existingPSSession = Get-PSSession | Where-Object {$_.ConfigurationName -like "Microsoft.Exchange" -and $_.Name -like "ExchangeOnlineInternalSession*"} if ($existingPSSession.count -gt 0) { for ($index = 0; $index -lt $existingPSSession.count; $index++) { $session = $existingPSSession[$index] Remove-PSSession -session $session Write-Information "Removed the PSSession $($session.Name) connected to $($session.ComputerName)" # Remove any active access token from the cache # If the connectionId of the session being cleared is equal to AppSettings.ConnectionId, this means connection to EXO cmdlets will break. if ($session.ConnectionContext.ConnectionId -ieq [Microsoft.Exchange.Management.AdminApiProvider.AppSettings]::ConnectionId) { Clear-ActiveToken -TokenProvider $session.TokenProvider -IsSessionUsedByInbuiltCmdlets:$true } else { Clear-ActiveToken -TokenProvider $session.TokenProvider -IsSessionUsedByInbuiltCmdlets:$false }
Es gibt also ein Commandlet "Clear-ActiveToken", welches im Modul "Microsoft.Exchange.Management.ExoPowershellGalleryModule.dll" verborgen ist und hier erst geladen wird.
$ConnectionContexts
Eine zweite interessante Stelle ist
# Get all the connection contexts so that the logger can be initialized. $connectionContexts = [Microsoft.Exchange.Management.ExoPowershellSnapin.ConnectionContextFactory]::GetAllConnectionContexts() $disconnectCmdletId = [System.Guid]::NewGuid().ToString()
Mich hat natürlich interessiert, was die Properties sind. In meinem Fall hab es nur genau einen "CconnectionContext" hat
PS C:> $connectionContexts.count 1
Interessant sind aber einige Properties. die ich in eigenen Skripten auch gut abfragen kann.
PS C:\> $connectionContexts | fl * ClientAppId : 604d9047-44e5-443a-ad8f-98abe5748b0a SessionPrefixName : ExchangeOnlineInternalSession_ ConnectionUri : https://outlook.office365.com PowerShellConnectionUri : https://outlook.office365.com:443/PowerShell-LiveID?BasicAuthToOAuthConversion=true&HideBannerMessage=true&ConnectionId=6e a29c98-b77e-4cd6-a7db-f35c73708464&ClientProcessId=27748&ExoModuleVersion=3.3.0&OSVersion=Microsoft+Windows+NT+10.0.22631.0 AzureAdAuthorizationEndpointUri : https://login.microsoftonline.com/organizations NewEXOModuleBaseUri : https://outlook.office365.com/AdminApi/v1.0 AdminApiBetaBaseUri : https://outlook.office365.com/AdminApi/beta ExoModuleVersion : 3.3.0 ExchangeEnvironmentName : O365Default IsCertBasedFlow : False IsCloudShellEnvironment : False TokenProvider : Microsoft.Exchange.Management.AdminApiProvider.Authentication.MSALTokenProvider PowerShellCredentials : System.Management.Automation.PSCredential PowerShellTokenInfo : Microsoft.Exchange.Management.AdminApiProvider.Authentication.TokenInformation TokenExpiryTime : 03.12.2023 17:21:52 +00:00 CommandName : {*} FormatTypeName : {*} Prefix : IsRpsSession : False IsEopSession : False ConnectionName : ExchangeOnline_1 PageSize : 1000 Logger : Microsoft.Online.CSE.RestApiPowerShellModule.Instrumentation.CmdletLogger UserPrincipalName : adminfcdev@msxfaqdev.onmicrosoft.com TenantId : fb78d390-0c51-40cd-8e17-fdbfab77341b
So kann ich von einer bestehenden Verbindung z.B. sehr schnell ermitteln, mit welcher Identität die Verbindung zu welchem Tenant aufgebaut wurde. Aber es gibt weitere Properties, die ich mich neugierig gemacht haben.
PowerShellTokenInfo
Das Property PowerShellTokenInfo habe ich mir genauer angeschaut und dort ist tatsächlich der UPN, die TenantID und das OAUTH-Token und die Gültigkeit
PS C:\> $connectionContexts.PowerShellTokenInfo | fl UserPrincipalName : adminfcdev@msxfaqdev.onmicrosoft.com AuthorizationHeader : Bearer eyJ0eXAiOiJKV1QiLCJub25............2Ag9 FWuaeeji45oFsjry5dNDv985fMd6Bk77GJ6lw8g TenantId : 604d9047-44e5-443a-ad8f-98abe5748b0a ExpiresOn : 03.12.2023 17:21:52 +00:00
Das OAUTH-Token im Feld "AuthorizationHeader" kann ich auch einfach mittels JWT.IO oder JWT.MS decodieren. Das Token ist ca. 26h! gültig und die "Expiration"-Zeit passt um Feld "ExpiresOn".
Die weiteren Felder (gekürzt) sind:
{ "aud": "https://outlook.office365.com", "iss": "https://sts.windows.net/604d9047-44e5-443a-ad8f-98abe5748b0a/", "iat": 1701531721, "nbf": 1701531721, "exp": 1701624111, "amr": ["pwd","mfa"], "app_displayname": "Microsoft Exchange REST API Based Powershell", "appid": "fb78d390-0c51-40cd-8e17-fdbfab77341b", ... "scp": "AdminApi.AccessAsUser.All FfoPowerShell.AccessAsUser.All RemotePowerShell.AccessAsUser.All VivaFeatureAccessPolicy.Manage.All", ... "upn": "adminfcdev@msxfaqdev.onmicrosoft.com", ... }
Mit dem Token kann ich nun natürlich auch eigene Invoke-WebRequest/Invoke-RestMethod-Aktionen auslösen, wenn z.B. bestimmte Funktionen nicht per PowerShell-Commandlet bereitgestellt sind.
Achtung:
Wenn ich als Benutzer das Token so auslesen kann, dann kann
es jedes Programm, welches in dieser PowerShell von mir
gestartet wird. Achten Sie daher darauf, welche anderen
Module sie nachladen oder welche anderen Commandlets sie
aufrufen und damit in diesem Prozess aktivieren. Ich warte
nur darauf, bis ein Modul in der PSGallery o.ä. so
Zugangsdaten gewinnt.
Ich habe danach die Session absichtlich länger als die 26h bestehen lassen und quasi zwei Tage später mir die Daten noch einmal angeschaut. Sie haben sich NICHT verändert, d.h. es gibt keinen Hintergrundprozess, welcher das Token bei Ablauf erneuert. Ich habe dann mit einem "Get-Mailbox" neue Daten angefordert aber selbst diese Aktion hat das Token nicht erneuert!
Das habe ich aber schon mehrfach gesehen, dass das Token nach der Anmeldung nur genutzt wird um einen Sessioncookie zu erhalten und danach das eigentliche AUTH-Token nicht mehr genutzt wird.
Logging
Bei der Analyse des Codes sind mir ein paar weitere Zeilen aufgefallen, die anscheinend eine Protokollierung bereitstellen.
foreach ($context in $connectionContexts) { $context.Logger.InitLog($disconnectCmdletId); $context.Logger.LogStartTime($disconnectCmdletId, $startTime); $context.Logger.LogCmdletName($disconnectCmdletId, "Disconnect-ExchangeOnline"); ...
Das "Logger"-Objekt hat dabei den Typ "Microsoft.Online.CSE.RestApiPowerShellModule.Instrumentation.CmdletLogger" mit folgenden Methoden und einem Property.
PS C:\> $connectionContexts.logger | gm TypeName: Microsoft.Online.CSE.RestApiPowerShellModule.Instrumentation.CmdletLogger Name MemberType Definition ---- ---------- ---------- CommitLog Method void CommitLog(string cmdletId), void ICmdletLogger.CommitLog(string cmdletId) Equals Method bool Equals(System.Object obj) GetCurrentLogFilePath Method string GetCurrentLogFilePath(), string ICmdletLogger.GetCurrentLogFilePath() GetHashCode Method int GetHashCode() GetType Method type GetType() InitLog Method void InitLog(string cmdletId), void ICmdletLogger.InitLog(string cmdletId) LogCmdletName Method void LogCmdletName(string cmdletId, string name), void ICmdletLogger.LogCmdletName(string cmdletId, string name) LogCmdletParameters Method void LogCmdletParameters(string cmdletId, string parameters), void LogCmdletParameters(string cmdletId, System.Collections… LogEndTime Method void LogEndTime(string cmdletId, datetime time), void ICmdletLogger.LogEndTime(string cmdletId, datetime time) LogGenericError Method void LogGenericError(string cmdletId, string error), void LogGenericError(string cmdletId, System.Management.Automation.Er… LogGenericInfo Method void LogGenericInfo(string cmdletId, string info), void ICmdletLogger.LogGenericInfo(string cmdletId, string info) LogStartTime Method void LogStartTime(string cmdletId, datetime time), void ICmdletLogger.LogStartTime(string cmdletId, datetime time) ToString Method string ToString() LogStore Property System.Collections.Concurrent.ConcurrentDictionary[string,Microsoft.Online.CSE.RestApiPowerShellModule.Instrumentation.ICm…
Ich kann sogar ohne Fehler eine Message senden.
PS C:\> $connectionContexts.logger.LogGenericInfo("MSXFAQ","Testmessage to logger")
Allerdings weiß ich noch nicht, wo Sie letztlich ankommt.
Im Hauptskript "ExchangeOnlineManagement.psm1" findet sich dazu eine ähnliche Logik:
$cmdletLogger = New-CmdletLogger ` -ExoModuleVersion $moduleVersion ` -LogDirectoryPath $LogDirectoryPath.Value ` -EnableErrorReporting:$EnableErrorReporting.Value ` -ConnectionId $connectionContextID ` -IsRpsSession:$UseRPSSession.IsPresent $logFilePath = $cmdletLogger.GetCurrentLogFilePath() if ($EnableErrorReporting.Value -eq $true -and $UseRPSSession -eq $false) { Write-Message ("Writing cmdlet logs to " + $logFilePath) }
Auch die Commandlets "New-CmdletLogger" oder "Write-Message" gehören nicht zum PowerShell Standard.
Interessanterweise gibt es noch eine weitere Protokollierungsfunktion.
Init-Log -CmdletId $CmdletRequestId; $startTime = Get-Date Log-Column -ColumnName StartTime -CmdletId $CmdletRequestId -Value $startTime Log-Column -ColumnName CmdletName -CmdletId $CmdletRequestId -Value $MyInvocation.MyCommand.Name Log-Column -ColumnName CmdletParameters -CmdletId $CmdletRequestId -Value $MyInvocation.BoundParameters Log-Column -ColumnName GenericError -CmdletId $CmdletRequestId -Value $_ Log-Column -ColumnName EndTime -CmdletId $CmdletRequestId -Value $endTime Commit-Log -CmdletId $CmdletRequestId $global:EXO_LastExecutionStatus = $false;
Das macht mir eher den Eindruck, das hier irgendeine "Zeiler einer Tabelle" erstellt und dann gespeichert wird. Leider gibt es zu der Funktion auch keine Dokumentation und sie ist auch nicht exportiert. Eine Suche im Modulverzeichnis "%userdata%\OneDrive\Dokumente\PowerShell\Modules\ExchangeOnlineManagement" nach dem String hat aber erst einmal nicht geliefert. Aber ein anderes Property hat mich dann auf die richtige Spur geschickt:
PS C:\> $connectionContexts.ModuleName C:\Users\fcarius\AppData\Local\Temp\tmpEXO_nvpskj4n.eem
In der Datei sind die Funktionen definiert und es ist schnell zu sehen, dass "Init-Log", "Log-Column" u.a. nur Hilfsfunktionen sind, um am Ende doch wieder "$connectionContexts.Logger" aufzurufen. Ein Mitschnitt mit Fiddler hat aber keine sichtbare Verbindung zur Cloud gezeigt.
Vielleicht kann ja jemand meiner Leser*innen hier etwas beisteuern, was sich hinter der Klasse "Microsoft.Online.CSE.RestApiPowerShellModule.Instrumentation.CmdletLogger" im Modul
Push-EXOTelemetryRecord
Gerade in Europa und insbesondere in Deutschland ist "Telemetrie" schon fast ein Alarmwort, auf das sich alle direkt stürzen. Dabei übersehen viele Menschen, dass allein die Nutzung von Smartphones und insbesondere der "sozialen Dienste", also Facebook, Twitter, TikTok etc viel mehr verwertbare Spuren hinterlassen, als die Sammlung von Aufrufen von Produkten zur berechtigten Fehlerbehebung, Qualitätssicherung und Verwendungsoptimierung. Das wird gerne als "Telemetrie" oder "Application Insights" bezeichnet aber die meisten PowerShell Entwickler hätten das vielleicht gerne aber über mehr als ein "Start-Transcript" oder einigen "Write-Host"-Ausgaben geht es dann noch nicht hinaus.
Für Microsoft ist es aber schon interessant zu wissen, welche Befehle wie genutzt werden. Daher überrascht es mich auch nicht, in dem Code entsprechende Hinweise zu solchen Funktionen zu finden, z.B.:
if ($rpsConnectionWithErrorReportingExists) { if ($global:_EXO_TelemetryFilePath -eq $null) { $global:_EXO_TelemetryFilePath = New-EXOClientTelemetryFilePath } # Log errors which are encountered during Disconnect-ExchangeOnline execution. Write-Message ("Writing Disconnect-ExchangeOnline errors to " + $global:_EXO_TelemetryFilePath) Push-EXOTelemetryRecord ` -TelemetryFilePath $global:_EXO_TelemetryFilePath ` -CommandName Disconnect-ExchangeOnline ` -CommandParams $PSCmdlet.MyInvocation.BoundParameters ` -OrganizationName $global:_EXO_ExPSTelemetryOrganization ` -ScriptName $global:_EXO_ExPSTelemetryScriptName ` -ScriptExecutionGuid $global:_EXO_ExPSTelemetryScriptExecutionGuid ` -ErrorObject $global:Error ` -ErrorRecordsToConsider ($errorCountAtProcessEnd ` -$errorCountAtStart) }
#Auszug aus C:\Users\username\OneDrive\Dokumente\PowerShell\Modules\ExchangeOnlineManagement\3.3.0\netCore\ExchangeOnlineManagement.psm1 # Where to store EXO command telemetry data. By default telemetry is stored in the directory "%TEMP%/EXOTelemetry" in the file : EXOCmdletTelemetry-yyyymmdd-hhmmss.csv. $LogDirectoryPath = New-Object System.Management.Automation.RuntimeDefinedParameter('LogDirectoryPath', [string], $attributeCollection) $LogDirectoryPath.Value = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "EXOCmdletTelemetry")
Aber auch hier habe ich auf die Schnelle nichts gefunden, wie die EXO-PowerShell diese Daten dann letztlich zu Microsoft oder einem lokalen Log schreibt. Ich hatte mir eigentlich erhofft, dass ich hier den ein oder anderen Kniff zur Nutzung von Telemetrie in meinen eigenen Skripten abschauen könnte.
Weitere Links
- Exchange PowerShell V3
- Developer Tenant
- EXO PowerShell Automation
- Conditional Access
- MFA und Dienstkonten
- App Password
- Exchange Online PowerShell V2
- Informationen zum Exchange Online PowerShell-Moduls
https://learn.microsoft.com/de-de/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps - Exchange Online PowerShell V3 Module General Availability
https://techcommunity.microsoft.com/t5/exchange-team-blog/exchange-online-powershell-v3-module-general-availability/bc-p/3743155