PowerShell-ReferenzPowerShell Reference

Modulübersicht, Verbindungsbeispiele und eine einsatzbereite Skriptsammlung für Entra ID, Microsoft 365 und Automatisierung.Module overview, connection examples, and a ready-to-run script collection for Entra ID, Microsoft 365, and automation.

Module und VerbindungsmusterModules and connection patterns

ModulModule InstallationInstall command VerbindungConnect command Erforderliche RechteRequired permissions
Microsoft.GraphMicrosoft.Graph Install-Module Microsoft.Graph -Scope CurrentUserInstall-Module Microsoft.Graph -Scope CurrentUser Connect-MgGraph -Scopes "User.Read.All"Connect-MgGraph -Scopes "User.Read.All" Graph Scopes und passende Entra-RollenGraph scopes and matching Entra roles
ExchangeOnlineManagementExchangeOnlineManagement Install-Module ExchangeOnlineManagement -Scope CurrentUserInstall-Module ExchangeOnlineManagement -Scope CurrentUser Connect-ExchangeOnline -UserPrincipalName admin@contoso.comConnect-ExchangeOnline -UserPrincipalName admin@contoso.com Exchange Admin oder delegierte RollenExchange admin or delegated roles
Microsoft.Online.SharePoint.PowerShellMicrosoft.Online.SharePoint.PowerShell Install-Module Microsoft.Online.SharePoint.PowerShell -Scope CurrentUserInstall-Module Microsoft.Online.SharePoint.PowerShell -Scope CurrentUser Connect-SPOService -Url https://contoso-admin.sharepoint.comConnect-SPOService -Url https://contoso-admin.sharepoint.com SharePoint AdminSharePoint admin
PnP.PowerShellPnP.PowerShell Install-Module PnP.PowerShell -Scope CurrentUserInstall-Module PnP.PowerShell -Scope CurrentUser Connect-PnPOnline -Url https://contoso.sharepoint.com -InteractiveConnect-PnPOnline -Url https://contoso.sharepoint.com -Interactive SharePoint-Berechtigungen oder App-RegistrierungSharePoint permissions or app registration
MicrosoftTeamsMicrosoftTeams Install-Module MicrosoftTeams -Scope CurrentUserInstall-Module MicrosoftTeams -Scope CurrentUser Connect-MicrosoftTeamsConnect-MicrosoftTeams Teams Admin oder passende Workload-RolleTeams admin or matching workload role
Microsoft.PowerApps.Administration.PowerShellMicrosoft.PowerApps.Administration.PowerShell Install-Module Microsoft.PowerApps.Administration.PowerShell -Scope CurrentUserInstall-Module Microsoft.PowerApps.Administration.PowerShell -Scope CurrentUser Add-PowerAppsAccountAdd-PowerAppsAccount Power Platform AdminPower Platform admin
AzAz Install-Module Az -Scope CurrentUserInstall-Module Az -Scope CurrentUser Connect-AzAccountConnect-AzAccount Azure RBAC und Subscription-ZugriffAzure RBAC and subscription access
PraxisPractice

Installiere Module bevorzugt im CurrentUser-Kontext und sperre Versionsstände in produktiven Automatisierungen, damit Runbooks und geplante Aufgaben reproduzierbar bleiben.Prefer installing modules in the CurrentUser scope and pin versions for production automation so runbooks and scheduled tasks remain reproducible.

Microsoft Graph PowerShellMicrosoft Graph PowerShell

Das Microsoft.Graph-Modul ist in Submodule aufgeteilt. Für kleine Admin-Workflows genügt oft das Meta-Modul; für schlanke Runbooks ist die Installation einzelner Submodule wie Microsoft.Graph.Users oder Microsoft.Graph.Identity.DirectoryManagement sinnvoll.The Microsoft.Graph module is split into submodules. For small admin workflows, the meta module is often enough; for lean runbooks, installing targeted submodules such as Microsoft.Graph.Users or Microsoft.Graph.Identity.DirectoryManagement is sensible.

Sub-ModulSubmodule Typische CmdletsTypical cmdlets EinsatzUse case
Microsoft.Graph.UsersMicrosoft.Graph.Users Get-MgUser, New-MgUserGet-MgUser, New-MgUser BenutzerverwaltungUser management
Microsoft.Graph.GroupsMicrosoft.Graph.Groups Get-MgGroup, New-MgGroupGet-MgGroup, New-MgGroup Gruppen und MitgliedschaftenGroups and memberships
Microsoft.Graph.Identity.SignInsMicrosoft.Graph.Identity.SignIns Get-MgAuditLogSignIn, Get-MgRiskyUserGet-MgAuditLogSignIn, Get-MgRiskyUser Audit, Sign-ins, Identity ProtectionAudit, sign-ins, identity protection
Microsoft.Graph.DeviceManagementMicrosoft.Graph.DeviceManagement Get-MgDeviceManagementManagedDeviceGet-MgDeviceManagementManagedDevice Intune und Managed DevicesIntune and managed devices
Microsoft.Graph.ApplicationsMicrosoft.Graph.Applications Get-MgApplication, Get-MgServicePrincipalGet-MgApplication, Get-MgServicePrincipal App-Registrierungen und Service PrincipalsApp registrations and service principals
PowerShellPowerShell

Install-Module Microsoft.Graph -Scope CurrentUser
Import-Module Microsoft.Graph
Connect-MgGraph -Scopes "User.Read.All","Group.Read.All","AuditLog.Read.All"
Get-MgContext
PowerShellPowerShell

Connect-MgGraph -TenantId $TenantId -ClientId $AppId -CertificateThumbprint $Thumbprint
Get-MgContext
Get-MgApplication -Top 5
PowerShellPowerShell

$clientSecret = ConvertTo-SecureString $env:GRAPH_CLIENT_SECRET -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($AppId, $clientSecret)
Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $credential
Get-MgServicePrincipal -Top 5
PowerShellPowerShell

Find-MgGraphCommand -Command Get-MgUser
Find-MgGraphCommand -Uri "/users/{id}/memberOf"
Find-MgGraphPermission -SearchString "Conditional Access"

Exchange OnlineExchange Online

ExchangeOnlineManagement ist das Standardmodul für Exchange Online. Viele Cmdlets sind REST-basiert und deutlich performanter als ältere Remote-PowerShell-Sitzungen.ExchangeOnlineManagement is the standard module for Exchange Online. Many cmdlets are REST-backed and significantly faster than legacy remote PowerShell sessions.

PowerShellPowerShell

Install-Module ExchangeOnlineManagement -Scope CurrentUser
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
Get-ConnectionInformation
CmdletCmdlet ZweckPurpose SyntaxSyntax
Connect-ExchangeOnlineConnect-ExchangeOnline Verbindung zu Exchange Online herstellenConnect to Exchange Online Connect-ExchangeOnline -UserPrincipalName admin@contoso.comConnect-ExchangeOnline -UserPrincipalName admin@contoso.com
Disconnect-ExchangeOnlineDisconnect-ExchangeOnline Sitzung sauber schließenClose the session cleanly Disconnect-ExchangeOnline -Confirm:$falseDisconnect-ExchangeOnline -Confirm:$false
Get-EXOMailboxGet-EXOMailbox Benutzer- und Spezialpostfächer lesenRead user and special mailboxes Get-EXOMailbox -ResultSize UnlimitedGet-EXOMailbox -ResultSize Unlimited
Get-MailboxGet-Mailbox Legacy-kompatible MailboxabfragenLegacy-compatible mailbox queries Get-Mailbox -ResultSize UnlimitedGet-Mailbox -ResultSize Unlimited
New-MailboxNew-Mailbox Postfach erstellenCreate a mailbox New-Mailbox -Name "Room 101" -RoomNew-Mailbox -Name "Room 101" -Room
Set-MailboxSet-Mailbox Postfach konfigurierenConfigure a mailbox Set-Mailbox -Identity user@contoso.com -HiddenFromAddressListsEnabled $trueSet-Mailbox -Identity user@contoso.com -HiddenFromAddressListsEnabled $true
Remove-MailboxRemove-Mailbox Postfach entfernenRemove a mailbox Remove-Mailbox -Identity user@contoso.comRemove-Mailbox -Identity user@contoso.com
Get-MailboxStatisticsGet-MailboxStatistics Größe und letzte Anmeldung ermittelnRetrieve size and last logon Get-MailboxStatistics -Identity user@contoso.comGet-MailboxStatistics -Identity user@contoso.com
Get-MailboxPermissionGet-MailboxPermission Full Access und andere Rechte prüfenInspect mailbox permissions Get-MailboxPermission -Identity shared@contoso.comGet-MailboxPermission -Identity shared@contoso.com
Add-MailboxPermissionAdd-MailboxPermission Mailboxrechte vergebenGrant mailbox permissions Add-MailboxPermission -Identity shared@contoso.com -User admin@contoso.com -AccessRights FullAccessAdd-MailboxPermission -Identity shared@contoso.com -User admin@contoso.com -AccessRights FullAccess
Remove-MailboxPermissionRemove-MailboxPermission Mailboxrechte entfernenRemove mailbox permissions Remove-MailboxPermission -Identity shared@contoso.com -User admin@contoso.com -AccessRights FullAccessRemove-MailboxPermission -Identity shared@contoso.com -User admin@contoso.com -AccessRights FullAccess
Get-RecipientPermissionGet-RecipientPermission Send As prüfenCheck Send As permissions Get-RecipientPermission -Identity shared@contoso.comGet-RecipientPermission -Identity shared@contoso.com
Add-RecipientPermissionAdd-RecipientPermission Send As vergebenGrant Send As Add-RecipientPermission -Identity shared@contoso.com -Trustee admin@contoso.com -AccessRights SendAsAdd-RecipientPermission -Identity shared@contoso.com -Trustee admin@contoso.com -AccessRights SendAs
Get-DistributionGroupGet-DistributionGroup Verteilergruppen lesenRead distribution groups Get-DistributionGroup -ResultSize UnlimitedGet-DistributionGroup -ResultSize Unlimited
New-DistributionGroupNew-DistributionGroup Verteilergruppe anlegenCreate a distribution group New-DistributionGroup -Name "IT Alerts" -PrimarySmtpAddress italerts@contoso.comNew-DistributionGroup -Name "IT Alerts" -PrimarySmtpAddress italerts@contoso.com
Set-DistributionGroupSet-DistributionGroup Verteilergruppe ändernModify a distribution group Set-DistributionGroup -Identity "IT Alerts" -ManagedBy admin@contoso.comSet-DistributionGroup -Identity "IT Alerts" -ManagedBy admin@contoso.com
Get-DistributionGroupMemberGet-DistributionGroupMember Mitglieder auslesenRead members Get-DistributionGroupMember -Identity "IT Alerts"Get-DistributionGroupMember -Identity "IT Alerts"
Add-DistributionGroupMemberAdd-DistributionGroupMember Mitglieder hinzufügenAdd members Add-DistributionGroupMember -Identity "IT Alerts" -Member user@contoso.comAdd-DistributionGroupMember -Identity "IT Alerts" -Member user@contoso.com
Get-TransportRuleGet-TransportRule Mailflow-Regeln lesenRead mail flow rules Get-TransportRuleGet-TransportRule
New-TransportRuleNew-TransportRule Mailflow-Regel anlegenCreate a mail flow rule New-TransportRule -Name "Block EXE" -AttachmentExtensionMatchesWords exe -RejectMessageReasonText "Executable blocked"New-TransportRule -Name "Block EXE" -AttachmentExtensionMatchesWords exe -RejectMessageReasonText "Executable blocked"
Set-TransportRuleSet-TransportRule Mailflow-Regel ändernModify a mail flow rule Set-TransportRule -Identity "Block EXE" -Mode EnforceSet-TransportRule -Identity "Block EXE" -Mode Enforce
Get-MessageTraceGet-MessageTrace Nachrichtenfluss suchenSearch message flow Get-MessageTrace -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date)Get-MessageTrace -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date)
Get-MessageTraceDetailGet-MessageTraceDetail Detail zum Trace abrufenGet message trace detail Get-MessageTraceDetail -MessageTraceId $trace.MessageTraceId -RecipientAddress $trace.RecipientAddressGet-MessageTraceDetail -MessageTraceId $trace.MessageTraceId -RecipientAddress $trace.RecipientAddress
Get-EXOMailboxFolderStatisticsGet-EXOMailboxFolderStatistics Ordnergrößen prüfenCheck folder sizes Get-EXOMailboxFolderStatistics -Identity user@contoso.comGet-EXOMailboxFolderStatistics -Identity user@contoso.com
Get-MailContactGet-MailContact Mailkontakte abrufenGet mail contacts Get-MailContact -ResultSize UnlimitedGet-MailContact -ResultSize Unlimited
Get-MailUserGet-MailUser MailUser abrufenGet mail users Get-MailUser -ResultSize UnlimitedGet-MailUser -ResultSize Unlimited
Get-EXORecipientGet-EXORecipient Empfänger aller Typen lesenRead recipients of all types Get-EXORecipient -ResultSize UnlimitedGet-EXORecipient -ResultSize Unlimited
Get-SharedMailboxGet-SharedMailbox Freigegebene Postfächer filternFilter shared mailboxes Get-EXOMailbox -RecipientTypeDetails SharedMailboxGet-EXOMailbox -RecipientTypeDetails SharedMailbox
Get-QuarantineMessageGet-QuarantineMessage Quarantäneobjekte lesenRead quarantine items Get-QuarantineMessage -PageSize 100Get-QuarantineMessage -PageSize 100
Get-EXOMailboxPermissionGet-EXOMailboxPermission REST-basierte BerechtigungsprüfungREST-based permission audit Get-EXOMailboxPermission -Identity shared@contoso.comGet-EXOMailboxPermission -Identity shared@contoso.com

SharePoint Online Management ShellSharePoint Online Management Shell

Für tenantweite SharePoint-Administration bleibt das SPO-Modul nützlich. Für moderne Site- und Inhaltsautomatisierung ist häufig PnP.PowerShell flexibler.For tenant-wide SharePoint administration, the SPO module remains useful. For modern site and content automation, PnP.PowerShell is often more flexible.

PowerShellPowerShell

Connect-SPOService -Url https://contoso-admin.sharepoint.com
Get-SPOSite -Limit All
CmdletCmdlet ZweckPurpose BeispielExample
Get-SPOSiteGet-SPOSite Sitesammlungen lesenRead site collections Get-SPOSite -Limit AllGet-SPOSite -Limit All
Set-SPOSiteSet-SPOSite Site-Einstellungen ändernUpdate site settings Set-SPOSite -Identity $SiteUrl -SharingCapability ExternalUserSharingOnlySet-SPOSite -Identity $SiteUrl -SharingCapability ExternalUserSharingOnly
Get-SPOUserGet-SPOUser Benutzer einer Site lesenRead site users Get-SPOUser -Site $SiteUrlGet-SPOUser -Site $SiteUrl
Set-SPOUserSet-SPOUser Site-Admin setzenSet site admin Set-SPOUser -Site $SiteUrl -LoginName admin@contoso.com -IsSiteCollectionAdmin $trueSet-SPOUser -Site $SiteUrl -LoginName admin@contoso.com -IsSiteCollectionAdmin $true
Get-SPODeletedSiteGet-SPODeletedSite Gelöschte Sites abrufenGet deleted sites Get-SPODeletedSiteGet-SPODeletedSite
Restore-SPODeletedSiteRestore-SPODeletedSite Gelöschte Site wiederherstellenRestore deleted site Restore-SPODeletedSite -Identity $SiteUrlRestore-SPODeletedSite -Identity $SiteUrl
Set-SPOTenantSet-SPOTenant Tenantweite Einstellungen anpassenAdjust tenant-wide settings Set-SPOTenant -SharingCapability ExistingExternalUserSharingOnlySet-SPOTenant -SharingCapability ExistingExternalUserSharingOnly
Get-SPOExternalUserGet-SPOExternalUser Externe Benutzer lesenRead external users Get-SPOExternalUser -SiteUrl $SiteUrlGet-SPOExternalUser -SiteUrl $SiteUrl

PnP.PowerShellPnP.PowerShell

PnP.PowerShell ist das vielseitigste Modul für SharePoint Online, Teams-nahe SharePoint-Strukturen und wiederverwendbare Site-Automatisierung. Es unterstützt Interactive Login, Zertifikate, Managed Identity und app-only Ansätze.PnP.PowerShell is the most versatile module for SharePoint Online, Teams-related SharePoint structures, and reusable site automation. It supports interactive login, certificates, managed identity, and app-only approaches.

AuthentifizierungAuthentication BeispielExample Wann sinnvoll?When useful?
InteraktivInteractive Connect-PnPOnline -Url https://contoso.sharepoint.com -InteractiveConnect-PnPOnline -Url https://contoso.sharepoint.com -Interactive Ad-hoc-AdministrationAd-hoc administration
ZertifikatCertificate Connect-PnPOnline -Url https://contoso.sharepoint.com -ClientId $AppId -Tenant contoso.onmicrosoft.com -Thumbprint $ThumbprintConnect-PnPOnline -Url https://contoso.sharepoint.com -ClientId $AppId -Tenant contoso.onmicrosoft.com -Thumbprint $Thumbprint Runbooks und produktive JobsRunbooks and production jobs
Managed IdentityManaged Identity Connect-PnPOnline -Url https://contoso.sharepoint.com -ManagedIdentityConnect-PnPOnline -Url https://contoso.sharepoint.com -ManagedIdentity Azure Automation und FunctionsAzure Automation and Functions
CmdletCmdlet ZweckPurpose BeispielExample
Get-PnPSiteGet-PnPSite Site-Metadaten lesenRead site metadata Get-PnPSiteGet-PnPSite
Get-PnPWebGet-PnPWeb Webobjekt lesenRead web object Get-PnPWebGet-PnPWeb
Get-PnPListGet-PnPList Listen abrufenGet lists Get-PnPListGet-PnPList
Get-PnPListItemGet-PnPListItem Listeneinträge abrufenGet list items Get-PnPListItem -List "Documents"Get-PnPListItem -List "Documents"
Add-PnPListItemAdd-PnPListItem Listeneintrag erstellenCreate list item Add-PnPListItem -List "Assets" -Values @{Title="Laptop-001"}Add-PnPListItem -List "Assets" -Values @{Title="Laptop-001"}
Set-PnPListItemSet-PnPListItem Listeneintrag aktualisierenUpdate list item Set-PnPListItem -List "Assets" -Identity 1 -Values @{Status="Assigned"}Set-PnPListItem -List "Assets" -Identity 1 -Values @{Status="Assigned"}
Get-PnPFolderItemGet-PnPFolderItem Dateien und Ordner lesenRead files and folders Get-PnPFolderItem -FolderSiteRelativeUrl "Shared Documents"Get-PnPFolderItem -FolderSiteRelativeUrl "Shared Documents"
Get-PnPRecycleBinItemGet-PnPRecycleBinItem Papierkorb prüfenInspect recycle bin Get-PnPRecycleBinItemGet-PnPRecycleBinItem

Microsoft Teams PowerShellMicrosoft Teams PowerShell

Das MicrosoftTeams-Modul deckt Richtlinien, Teams-Objekte, Kanäle, Telefonie und viele Tenant-Einstellungen ab. In der Praxis wird es oft mit Graph und Exchange kombiniert.The MicrosoftTeams module covers policies, teams objects, channels, telephony, and many tenant settings. In practice, it is often combined with Graph and Exchange.

PowerShellPowerShell

Connect-MicrosoftTeams
Get-CsTenant
CmdletCmdlet ZweckPurpose BeispielExample
Get-TeamGet-Team Teams lesenRead teams Get-TeamGet-Team
New-TeamNew-Team Team erstellenCreate a team New-Team -DisplayName "Operations" -Visibility PrivateNew-Team -DisplayName "Operations" -Visibility Private
Set-TeamSet-Team Team konfigurierenConfigure a team Set-Team -GroupId $GroupId -AllowCreateUpdateChannels $falseSet-Team -GroupId $GroupId -AllowCreateUpdateChannels $false
Get-TeamChannelGet-TeamChannel Kanäle lesenRead channels Get-TeamChannel -GroupId $GroupIdGet-TeamChannel -GroupId $GroupId
New-TeamChannelNew-TeamChannel Kanal anlegenCreate a channel New-TeamChannel -GroupId $GroupId -DisplayName "Projekt"New-TeamChannel -GroupId $GroupId -DisplayName "Projekt"
Get-CsTeamsMessagingPolicyGet-CsTeamsMessagingPolicy Messaging Policies lesenRead messaging policies Get-CsTeamsMessagingPolicyGet-CsTeamsMessagingPolicy
Get-CsTeamsMeetingPolicyGet-CsTeamsMeetingPolicy Meeting Policies lesenRead meeting policies Get-CsTeamsMeetingPolicyGet-CsTeamsMeetingPolicy
Grant-CsTeamsMeetingPolicyGrant-CsTeamsMeetingPolicy Meeting Policy zuweisenAssign meeting policy Grant-CsTeamsMeetingPolicy -Identity user@contoso.com -PolicyName RestrictedMeetingGrant-CsTeamsMeetingPolicy -Identity user@contoso.com -PolicyName RestrictedMeeting
Get-CsOnlineUserGet-CsOnlineUser Voice/Teams-Benutzer prüfenInspect voice/Teams users Get-CsOnlineUser -Identity user@contoso.comGet-CsOnlineUser -Identity user@contoso.com
Get-CsPhoneNumberAssignmentGet-CsPhoneNumberAssignment Rufnummernzuweisungen lesenRead number assignments Get-CsPhoneNumberAssignmentGet-CsPhoneNumberAssignment

Komplettes VerbindungsskriptComplete connection script

PowerShellPowerShell

$tenantId = "contoso.onmicrosoft.com"
$adminUpn = "admin@contoso.com"
$spoAdminUrl = "https://contoso-admin.sharepoint.com"
$siteUrl = "https://contoso.sharepoint.com/sites/Operations"

Import-Module Microsoft.Graph
Import-Module ExchangeOnlineManagement
Import-Module Microsoft.Online.SharePoint.PowerShell
Import-Module PnP.PowerShell
Import-Module MicrosoftTeams

Connect-MgGraph -Scopes "User.Read.All","Group.Read.All","Directory.Read.All","AuditLog.Read.All"
Connect-ExchangeOnline -UserPrincipalName $adminUpn
Connect-SPOService -Url $spoAdminUrl
Connect-PnPOnline -Url $siteUrl -Interactive
Connect-MicrosoftTeams

Write-Host "Graph context:" (Get-MgContext).TenantId
Write-Host "Exchange connected:" ((Get-ConnectionInformation).State -join ", ")
Write-Host "Teams tenant:" (Get-CsTenant).DisplayName

SkriptsammlungScript collection

Alle Benutzer nach CSV exportierenExport all users to CSV

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

$properties = @(
    "id","displayName","givenName","surname","userPrincipalName","mail",
    "department","jobTitle","officeLocation","accountEnabled","usageLocation",
    "city","country","mobilePhone","businessPhones","onPremisesSyncEnabled"
)

Connect-MgGraph -Scopes "User.Read.All"
$users = Get-MgUser -All -Property $properties

$report = foreach ($user in $users) {
    [pscustomobject]@{
        Id                   = $user.Id
        DisplayName          = $user.DisplayName
        GivenName            = $user.GivenName
        Surname              = $user.Surname
        UserPrincipalName    = $user.UserPrincipalName
        Mail                 = $user.Mail
        Department           = $user.Department
        JobTitle             = $user.JobTitle
        OfficeLocation       = $user.OfficeLocation
        AccountEnabled       = $user.AccountEnabled
        UsageLocation        = $user.UsageLocation
        City                 = $user.City
        Country              = $user.Country
        MobilePhone          = $user.MobilePhone
        OnPremisesSyncEnabled= $user.OnPremisesSyncEnabled
    }
}

$report | Export-Csv .\all-users.csv -NoTypeInformation -Encoding UTF8

Inaktive Benutzer 90+ Tage findenFind inactive users for 90+ days

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-MgGraph -Scopes "User.Read.All","AuditLog.Read.All"
Select-MgProfile -Name beta
$cutoff = (Get-Date).AddDays(-90)
$users = Get-MgUser -All -Property "displayName,userPrincipalName,accountEnabled,signInActivity"

$inactive = foreach ($user in $users) {
    $last = $null
    if ($user.SignInActivity.lastSignInDateTime) {
        $last = [datetime]$user.SignInActivity.lastSignInDateTime
    }

    if (-not $last -or $last -lt $cutoff) {
        [pscustomobject]@{
            DisplayName       = $user.DisplayName
            UserPrincipalName = $user.UserPrincipalName
            AccountEnabled    = $user.AccountEnabled
            LastSignIn        = $last
            DaysInactive      = if ($last) { ((Get-Date) - $last).Days } else { $null }
        }
    }
}

$inactive | Sort-Object DaysInactive -Descending | Export-Csv .\inactive-users.csv -NoTypeInformation

Benutzer aus CSV massenhaft anlegenBulk create users from CSV

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-MgGraph -Scopes "User.ReadWrite.All"
$users = Import-Csv .\new-users.csv

foreach ($row in $users) {
    $passwordProfile = @{
        forceChangePasswordNextSignIn = $true
        password = $row.InitialPassword
    }

    New-MgUser -AccountEnabled:$true `
        -DisplayName $row.DisplayName `
        -GivenName $row.GivenName `
        -Surname $row.Surname `
        -MailNickname $row.MailNickname `
        -UserPrincipalName $row.UserPrincipalName `
        -Department $row.Department `
        -JobTitle $row.JobTitle `
        -UsageLocation $row.UsageLocation `
        -PasswordProfile $passwordProfile

    Write-Host "Created $($row.UserPrincipalName)"
}

Alle Gruppen und Mitglieder exportierenExport all groups and members

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-MgGraph -Scopes "Group.Read.All","GroupMember.Read.All"
$groups = Get-MgGroup -All -Property "id,displayName,groupTypes,mail,mailEnabled,securityEnabled"
$rows = foreach ($group in $groups) {
    $members = Get-MgGroupMember -GroupId $group.Id -All
    foreach ($member in $members) {
        [pscustomobject]@{
            GroupId          = $group.Id
            GroupDisplayName = $group.DisplayName
            GroupMail        = $group.Mail
            SecurityEnabled  = $group.SecurityEnabled
            MailEnabled      = $group.MailEnabled
            MemberId         = $member.Id
            MemberType       = $member.AdditionalProperties['@odata.type']
        }
    }
}
$rows | Export-Csv .\groups-and-members.csv -NoTypeInformation -Encoding UTF8

Lizenzzuweisungsbericht erzeugenCreate license assignment report

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-MgGraph -Scopes "User.Read.All","Organization.Read.All"
$skuMap = @{}
Get-MgSubscribedSku -All | ForEach-Object {
    $skuMap[$_.SkuId.Guid] = $_.SkuPartNumber
}

$users = Get-MgUser -All -Property "displayName,userPrincipalName,assignedLicenses,usageLocation"
$report = foreach ($user in $users) {
    [pscustomobject]@{
        DisplayName       = $user.DisplayName
        UserPrincipalName = $user.UserPrincipalName
        UsageLocation     = $user.UsageLocation
        Licenses          = ($user.AssignedLicenses.SkuId.Guid | ForEach-Object { $skuMap[$_] }) -join '; '
        LicenseCount      = $user.AssignedLicenses.Count
    }
}
$report | Export-Csv .\license-assignment-report.csv -NoTypeInformation

MFA-Registrierungsstatus berichtenReport MFA registration status

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-MgGraph -Scopes "Reports.Read.All"
$rows = Get-MgReportAuthenticationMethodUserRegistrationDetail -All

$report = $rows | Select-Object `
    UserPrincipalName,
    UserDisplayName,
    IsMfaRegistered,
    IsSsprRegistered,
    IsPasswordlessCapable,
    IsAdmin,
    DefaultMfaMethod,
    MethodsRegistered

$report | Export-Csv .\mfa-registration-status.csv -NoTypeInformation -Encoding UTF8

Conditional-Access-Richtlinien nach JSON exportierenExport conditional access policies to JSON

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-MgGraph -Scopes "Policy.Read.All"
$policies = Get-MgIdentityConditionalAccessPolicy -All

$export = foreach ($policy in $policies) {
    [pscustomobject]@{
        Id          = $policy.Id
        DisplayName = $policy.DisplayName
        State       = $policy.State
        Conditions  = $policy.Conditions
        GrantControls = $policy.GrantControls
        SessionControls = $policy.SessionControls
    }
}

$export | ConvertTo-Json -Depth 20 | Set-Content .\conditional-access-policies.json -Encoding UTF8

Postfachberechtigungen auditierenAudit mailbox permissions

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
$mailboxes = Get-EXOMailbox -ResultSize Unlimited
$report = foreach ($mailbox in $mailboxes) {
    $permissions = Get-MailboxPermission -Identity $mailbox.UserPrincipalName |
        Where-Object { $_.User -notlike 'NT AUTHORITY*' -and -not $_.IsInherited }

    foreach ($permission in $permissions) {
        [pscustomobject]@{
            Mailbox     = $mailbox.UserPrincipalName
            Trustee     = $permission.User
            AccessRights= ($permission.AccessRights -join ', ')
            Deny        = $permission.Deny
        }
    }
}
$report | Export-Csv .\mailbox-permissions.csv -NoTypeInformation -Encoding UTF8

Freigegebene Postfächer vollständig berichtenCreate shared mailbox full report

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
$sharedMailboxes = Get-EXOMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited

$report = foreach ($mailbox in $sharedMailboxes) {
    $stats = Get-EXOMailboxStatistics -Identity $mailbox.UserPrincipalName
    [pscustomobject]@{
        DisplayName          = $mailbox.DisplayName
        PrimarySmtpAddress   = $mailbox.PrimarySmtpAddress
        HiddenFromAddressList= $mailbox.HiddenFromAddressListsEnabled
        LitigationHold       = $mailbox.LitigationHoldEnabled
        ArchiveStatus        = $mailbox.ArchiveStatus
        ItemCount            = $stats.ItemCount
        TotalItemSize        = $stats.TotalItemSize
        LastLogonTime        = $stats.LastLogonTime
    }
}
$report | Export-Csv .\shared-mailbox-report.csv -NoTypeInformation -Encoding UTF8

Gastbenutzer mit letzter Anmeldung berichtenReport guest users with last sign-in

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-MgGraph -Scopes "User.Read.All","AuditLog.Read.All"
Select-MgProfile -Name beta
$guests = Get-MgUser -All -Filter "userType eq 'Guest'" -Property "displayName,userPrincipalName,mail,createdDateTime,signInActivity"

$report = foreach ($guest in $guests) {
    [pscustomobject]@{
        DisplayName       = $guest.DisplayName
        UserPrincipalName = $guest.UserPrincipalName
        Mail              = $guest.Mail
        CreatedDateTime   = $guest.CreatedDateTime
        LastSignIn        = $guest.SignInActivity.lastSignInDateTime
    }
}

$report | Export-Csv .\guest-users-last-signin.csv -NoTypeInformation -Encoding UTF8

Veraltete Geräte bereinigenClean up stale devices

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-MgGraph -Scopes "Device.ReadWrite.All","DeviceManagementManagedDevices.Read.All"
$cutoff = (Get-Date).AddDays(-120)
$devices = Get-MgDevice -All -Property "id,displayName,approximateLastSignInDateTime,accountEnabled"

$stale = $devices | Where-Object {
    $_.ApproximateLastSignInDateTime -and ([datetime]$_.ApproximateLastSignInDateTime) -lt $cutoff
}

$stale | Select-Object Id, DisplayName, ApproximateLastSignInDateTime, AccountEnabled |
    Export-Csv .\stale-devices.csv -NoTypeInformation

# Review CSV first, then uncomment the next line for cleanup
# $stale | ForEach-Object { Update-MgDevice -DeviceId $_.Id -AccountEnabled:$false }

Lizenzen aus CSV massenhaft zuweisenBulk assign licenses from CSV

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-MgGraph -Scopes "User.ReadWrite.All","Organization.Read.All"
$skuMap = Get-MgSubscribedSku -All | Group-Object SkuPartNumber -AsHashTable -AsString
$rows = Import-Csv .\license-assignments.csv

foreach ($row in $rows) {
    $sku = $skuMap[$row.SkuPartNumber]
    if (-not $sku) {
        Write-Warning "SKU $($row.SkuPartNumber) not found"
        continue
    }

    $body = @{
        addLicenses = @(@{ skuId = $sku.SkuId })
        removeLicenses = @()
    }

    Set-MgUserLicense -UserId $row.UserPrincipalName -BodyParameter $body
    Write-Host "Assigned $($row.SkuPartNumber) to $($row.UserPrincipalName)"
}

Sicherheitsgruppen-Mitgliedschaften vergleichenCompare security group memberships

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

$groupAId = "11111111-1111-1111-1111-111111111111"
$groupBId = "22222222-2222-2222-2222-222222222222"
Connect-MgGraph -Scopes "GroupMember.Read.All"

$groupA = Get-MgGroupMember -GroupId $groupAId -All | Select-Object -ExpandProperty Id
$groupB = Get-MgGroupMember -GroupId $groupBId -All | Select-Object -ExpandProperty Id

$onlyA = $groupA | Where-Object { $_ -notin $groupB }
$onlyB = $groupB | Where-Object { $_ -notin $groupA }

[pscustomobject]@{ Comparison = "OnlyInA"; Count = $onlyA.Count }
[pscustomobject]@{ Comparison = "OnlyInB"; Count = $onlyB.Count }

$onlyA | Set-Content .\group-a-only.txt
$onlyB | Set-Content .\group-b-only.txt

App-Registrierungen mit Secret-Ablauf berichtenReport app registration secret expiry

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-MgGraph -Scopes "Application.Read.All"
$applications = Get-MgApplication -All -Property "id,displayName,passwordCredentials,keyCredentials"
$today = Get-Date

$report = foreach ($app in $applications) {
    foreach ($secret in $app.PasswordCredentials) {
        [pscustomobject]@{
            AppDisplayName = $app.DisplayName
            AppId          = $app.Id
            CredentialType = "ClientSecret"
            EndDateTime    = $secret.EndDateTime
            DaysRemaining  = ([datetime]$secret.EndDateTime - $today).Days
        }
    }
}

$report | Sort-Object DaysRemaining | Export-Csv .\app-secret-expiry.csv -NoTypeInformation

Benutzer-Anmeldeaktivitäten berichtenReport user sign-in activity

Dieses Skript ist als sofort nutzbare Vorlage gedacht und kann direkt in eine Admin-Shell oder in ein Runbook übernommen werden.This script is intended as an immediately usable template and can be copied into an admin shell or runbook.

PowerShellPowerShell

Connect-MgGraph -Scopes "AuditLog.Read.All","User.Read.All"
$since = (Get-Date).AddDays(-30)
$signIns = Get-MgAuditLogSignIn -All -Filter "createdDateTime ge $($since.ToString("s"))Z"

$report = $signIns | Group-Object UserPrincipalName | ForEach-Object {
    $latest = $_.Group | Sort-Object CreatedDateTime -Descending | Select-Object -First 1
    [pscustomobject]@{
        UserPrincipalName = $_.Name
        SignInCount       = $_.Count
        LastSignIn        = $latest.CreatedDateTime
        LastApp           = $latest.AppDisplayName
        LastIpAddress     = $latest.IpAddress
        LastStatus        = $latest.Status.ErrorCode
    }
}

$report | Export-Csv .\user-signin-activity.csv -NoTypeInformation -Encoding UTF8

20 More Complete Scripts20 More Complete Scripts

Die folgenden Skripte sind als produktionsnahe Vorlagen gedacht: mit Fehlerbehandlung, kommentierten Schritten und CSV-/JSON-Export. Passe Tenant-spezifische Scope-Sets, Delays und Ausgabeorte an deine Betriebsrichtlinien an.The following scripts are designed as production-oriented templates with error handling, commented steps, and CSV/JSON export. Adjust tenant-specific scope sets, delays, and output paths to match your operational standards.

HinweisNote

Viele Skripte kombinieren Microsoft Graph, Exchange Online, Teams oder SharePoint. In produktiven Runbooks sollten Modulversionen fixiert, Ausgaben zentral protokolliert und Secrets ausschließlich über verwaltete Identitäten oder sichere Secret Stores bezogen werden.Many scripts combine Microsoft Graph, Exchange Online, Teams, or SharePoint. In production runbooks, pin module versions, centralize logging, and obtain secrets only from managed identities or secure secret stores.

Conditional Access Policy BackupConditional Access Policy Backup

Exportiert alle Conditional-Access-Richtlinien als einzelne JSON-Dateien plus Inventar-CSV.Exports all Conditional Access policies as individual JSON files plus an inventory CSV.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$OutputFolder = ".\ca-policy-backup"
)

try {
    # Load module and connect to Microsoft Graph
    Import-Module Microsoft.Graph.Identity.SignIns -ErrorAction Stop
    Connect-MgGraph -Scopes "Policy.Read.All" -NoWelcome -ErrorAction Stop

    if (-not (Test-Path $OutputFolder)) {
        New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null
    }

    $policies = Get-MgIdentityConditionalAccessPolicy -All -ErrorAction Stop
    $inventory = foreach ($policy in $policies) {
        $safeName = ($policy.DisplayName -replace '[^a-zA-Z0-9-_ ]','_').Trim()
        $jsonPath = Join-Path $OutputFolder "$safeName.json"

        # Persist full object for restore or diff scenarios
        $policy | ConvertTo-Json -Depth 25 | Set-Content -Path $jsonPath -Encoding UTF8

        [pscustomobject]@{
            DisplayName = $policy.DisplayName
            Id          = $policy.Id
            State       = $policy.State
            OutputFile  = $jsonPath
        }
    }

    $inventory | Export-Csv (Join-Path $OutputFolder 'ca-policy-inventory.csv') -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Conditional Access backup failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

Stale Guest User CleanupStale Guest User Cleanup

Findet Gastkonten ohne Anmeldung in den letzten 180 Tagen, exportiert sie und entfernt sie optional.Finds guest accounts with no sign-in during the last 180 days, exports them, and optionally removes them.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [int]$InactiveDays = 180,
    [switch]$RemoveAccounts,
    [string]$OutputPath = ".\stale-guest-users.csv"
)

try {
    # Connect and load all guest users with sign-in activity
    Import-Module Microsoft.Graph.Users, Microsoft.Graph.Identity.SignIns -ErrorAction Stop
    Connect-MgGraph -Scopes "User.Read.All","AuditLog.Read.All","User.ReadWrite.All" -NoWelcome -ErrorAction Stop

    $cutoff = (Get-Date).AddDays(-$InactiveDays)
    $guests = Get-MgUser -All -Filter "userType eq 'Guest'" -Property Id,DisplayName,UserPrincipalName,CreatedDateTime,SignInActivity

    $staleGuests = $guests | Where-Object {
        -not $_.SignInActivity.LastSignInDateTime -or ([datetime]$_.SignInActivity.LastSignInDateTime) -lt $cutoff
    } | Select-Object DisplayName, UserPrincipalName, CreatedDateTime,
        @{Name='LastSignInDateTime';Expression={$_.SignInActivity.LastSignInDateTime}},
        @{Name='DaysInactive';Expression={ if ($_.SignInActivity.LastSignInDateTime) { ((Get-Date) - [datetime]$_.SignInActivity.LastSignInDateTime).Days } else { 9999 } }}

    $staleGuests | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8

    if ($RemoveAccounts) {
        foreach ($guest in $staleGuests) {
            # Remove only after review and explicit switch usage
            Remove-MgUser -UserId $guest.UserPrincipalName -ErrorAction Stop
        }
    }
}
catch {
    Write-Error "Guest cleanup failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

App Registration Secret Expiry ReportApp Registration Secret Expiry Report

Meldet alle App-Registrierungen mit Secrets, die in 30, 60 oder 90 Tagen ablaufen.Reports all app registrations with secrets expiring in 30, 60, or 90 days.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [int[]]$ThresholdDays = @(30,60,90),
    [string]$OutputPath = ".\app-secret-expiry-report.csv"
)

try {
    # Load applications and inspect password credentials
    Import-Module Microsoft.Graph.Applications -ErrorAction Stop
    Connect-MgGraph -Scopes "Application.Read.All" -NoWelcome -ErrorAction Stop

    $today = Get-Date
    $maxThreshold = ($ThresholdDays | Measure-Object -Maximum).Maximum
    $applications = Get-MgApplication -All -Property Id,DisplayName,AppId,PasswordCredentials

    $report = foreach ($app in $applications) {
        foreach ($secret in $app.PasswordCredentials) {
            $daysRemaining = ([datetime]$secret.EndDateTime - $today).Days
            if ($daysRemaining -le $maxThreshold) {
                [pscustomobject]@{
                    DisplayName   = $app.DisplayName
                    ApplicationId = $app.AppId
                    ObjectId      = $app.Id
                    SecretName    = $secret.DisplayName
                    EndDateTime   = $secret.EndDateTime
                    DaysRemaining = $daysRemaining
                    Bucket        = ($ThresholdDays | Where-Object { $daysRemaining -le $_ } | Sort-Object | Select-Object -First 1)
                }
            }
        }
    }

    $report | Sort-Object DaysRemaining | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Secret expiry report failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

Group Membership Delta ReportGroup Membership Delta Report

Vergleicht zwei exportierte Gruppensnapshots und zeigt neue sowie entfernte Mitglieder.Compares two exported group snapshots and shows newly added and removed members.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [Parameter(Mandatory)]
    [string]$BeforeSnapshot,
    [Parameter(Mandatory)]
    [string]$AfterSnapshot,
    [string]$OutputPath = ".\group-membership-delta.csv"
)

try {
    # Import two snapshots and compare member state per group
    $before = Import-Csv $BeforeSnapshot -ErrorAction Stop
    $after  = Import-Csv $AfterSnapshot -ErrorAction Stop

    $delta = foreach ($groupId in ($after.GroupId + $before.GroupId | Sort-Object -Unique)) {
        $beforeMembers = $before | Where-Object GroupId -eq $groupId | Select-Object -ExpandProperty MemberUPN
        $afterMembers  = $after  | Where-Object GroupId -eq $groupId | Select-Object -ExpandProperty MemberUPN
        $groupName = ($after | Where-Object GroupId -eq $groupId | Select-Object -ExpandProperty GroupName -First 1)
        if (-not $groupName) {
            $groupName = ($before | Where-Object GroupId -eq $groupId | Select-Object -ExpandProperty GroupName -First 1)
        }

        Compare-Object -ReferenceObject $beforeMembers -DifferenceObject $afterMembers -PassThru | ForEach-Object {
            [pscustomobject]@{
                GroupId    = $groupId
                GroupName  = $groupName
                MemberUPN  = $_
                ChangeType = if ($_.SideIndicator -eq '=>') { 'Added' } else { 'Removed' }
            }
        }
    }

    $delta | Sort-Object GroupName, ChangeType, MemberUPN | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Group delta report failed: $($_.Exception.Message)"
    throw
}

Mailbox Size ReportMailbox Size Report

Erstellt einen Exchange-Report mit Mailboxgröße, Elementanzahl und letzter Aktivität.Builds an Exchange report with mailbox size, item count, and last activity.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$OutputPath = ".\mailbox-size-report.csv"
)

try {
    # Connect to Exchange Online and enumerate user mailboxes
    Import-Module ExchangeOnlineManagement -ErrorAction Stop
    Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop

    $mailboxes = Get-EXOMailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox
    $report = foreach ($mailbox in $mailboxes) {
        $stats = Get-EXOMailboxStatistics -Identity $mailbox.UserPrincipalName -ErrorAction Stop
        [pscustomobject]@{
            DisplayName        = $mailbox.DisplayName
            UserPrincipalName  = $mailbox.UserPrincipalName
            PrimarySmtpAddress = $mailbox.PrimarySmtpAddress
            TotalItemSize      = $stats.TotalItemSize
            ItemCount          = $stats.ItemCount
            LastLogonTime      = $stats.LastLogonTime
            LastUserActionTime = $stats.LastUserActionTime
        }
    }

    $report | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Mailbox size report failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
}

Teams Ownership ReportTeams Ownership Report

Ermittelt Besitzeranzahl, Mitgliederanzahl und letzte Aktivität für alle Teams.Calculates owner count, member count, and last activity for all teams.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$OutputPath = ".\teams-ownership-report.csv"
)

try {
    # Connect to Teams and inspect owner/member ratios
    Import-Module MicrosoftTeams -ErrorAction Stop
    Connect-MicrosoftTeams -ErrorAction Stop

    $teams = Get-Team
    $report = foreach ($team in $teams) {
        $users = Get-TeamUser -GroupId $team.GroupId
        [pscustomobject]@{
            DisplayName  = $team.DisplayName
            GroupId      = $team.GroupId
            OwnerCount   = ($users | Where-Object Role -eq 'Owner').Count
            MemberCount  = ($users | Where-Object Role -eq 'Member').Count
            Visibility   = $team.Visibility
            Archived     = $team.Archived
            LastActivity = $team.LastActivityDate
        }
    }

    $report | Sort-Object OwnerCount, MemberCount -Descending | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Teams ownership report failed: $($_.Exception.Message)"
    throw
}

SharePoint Storage ReportSharePoint Storage Report

Exportiert alle SharePoint-Sites mit belegtem und zugewiesenem Speicher.Exports all SharePoint sites with used and allocated storage.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [Parameter(Mandatory)]
    [string]$AdminUrl,
    [string]$OutputPath = ".\sharepoint-storage-report.csv"
)

try {
    # Connect to SharePoint Online admin service
    Import-Module Microsoft.Online.SharePoint.PowerShell -ErrorAction Stop
    Connect-SPOService -Url $AdminUrl -ErrorAction Stop

    $sites = Get-SPOSite -Limit All -Detailed
    $report = $sites | Select-Object Url, Title, Owner,
        @{Name='StorageMB';Expression={$_.StorageUsageCurrent}},
        @{Name='StorageQuotaMB';Expression={$_.StorageQuota}},
        @{Name='Template';Expression={$_.Template}},
        @{Name='LastContentModifiedDate';Expression={$_.LastContentModifiedDate}}

    $report | Sort-Object StorageMB -Descending | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "SharePoint storage report failed: $($_.Exception.Message)"
    throw
}

Conditional Access Gap AnalysisConditional Access Gap Analysis

Markiert aktive Benutzer, die von keiner produktiven CA-Richtlinie explizit erfasst werden.Flags active users not explicitly covered by any production Conditional Access policy.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$OutputPath = ".\conditional-access-gap-analysis.csv"
)

try {
    # Load active member users and enabled CA policies
    Import-Module Microsoft.Graph.Users, Microsoft.Graph.Groups, Microsoft.Graph.Identity.SignIns -ErrorAction Stop
    Connect-MgGraph -Scopes "User.Read.All","Group.Read.All","Policy.Read.All" -NoWelcome -ErrorAction Stop

    $users = Get-MgUser -All -Filter "accountEnabled eq true and userType eq 'Member'" -Property Id,DisplayName,UserPrincipalName
    $policies = Get-MgIdentityConditionalAccessPolicy -All | Where-Object State -ne 'disabled'

    $coveredUserIds = New-Object System.Collections.Generic.HashSet[string]
    foreach ($policy in $policies) {
        foreach ($userId in $policy.Conditions.Users.IncludeUsers) { [void]$coveredUserIds.Add($userId) }
        foreach ($groupId in $policy.Conditions.Users.IncludeGroups) {
            Get-MgGroupMember -GroupId $groupId -All | ForEach-Object { [void]$coveredUserIds.Add($_.Id) }
        }
    }

    $gaps = $users | Where-Object { -not $coveredUserIds.Contains($_.Id) } | Select-Object DisplayName, UserPrincipalName, Id
    $gaps | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Conditional Access gap analysis failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

License Reclamation ScriptLicense Reclamation Script

Findet lizenzierte, aber seit 90+ Tagen inaktive Benutzer für Rückgewinnungskampagnen.Finds licensed but inactive users for 90+ day reclamation campaigns.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [int]$InactiveDays = 90,
    [string]$OutputPath = ".\license-reclamation-report.csv"
)

try {
    # Load user sign-in activity and assigned licenses
    Import-Module Microsoft.Graph.Users -ErrorAction Stop
    Connect-MgGraph -Scopes "User.Read.All","Reports.Read.All" -NoWelcome -ErrorAction Stop

    $cutoff = (Get-Date).AddDays(-$InactiveDays)
    $users = Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,AssignedLicenses,SignInActivity,AccountEnabled

    $report = $users | Where-Object {
        $_.AccountEnabled -and $_.AssignedLicenses.Count -gt 0 -and (
            -not $_.SignInActivity.LastSuccessfulSignInDateTime -or
            ([datetime]$_.SignInActivity.LastSuccessfulSignInDateTime) -lt $cutoff
        )
    } | Select-Object DisplayName, UserPrincipalName,
        @{Name='LicenseCount';Expression={$_.AssignedLicenses.Count}},
        @{Name='LastSuccessfulSignIn';Expression={$_.SignInActivity.LastSuccessfulSignInDateTime}},
        @{Name='DaysInactive';Expression={ if ($_.SignInActivity.LastSuccessfulSignInDateTime) { ((Get-Date) - [datetime]$_.SignInActivity.LastSuccessfulSignInDateTime).Days } else { 9999 } }}

    $report | Sort-Object DaysInactive -Descending | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "License reclamation report failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

Privileged Role Assignment ReportPrivileged Role Assignment Report

Listet permanente und eligible Admin-Rollenzuweisungen aus Entra ID und PIM.Lists permanent and eligible admin role assignments from Entra ID and PIM.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$OutputPath = ".\privileged-role-assignments.csv"
)

try {
    # Read active and eligible directory role assignments
    Import-Module Microsoft.Graph.Identity.Governance -ErrorAction Stop
    Connect-MgGraph -Scopes "RoleManagement.Read.All","Directory.Read.All" -NoWelcome -ErrorAction Stop

    $activeAssignments = Get-MgRoleManagementDirectoryRoleAssignmentSchedule -All
    $eligibleAssignments = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All

    $report = @()
    foreach ($assignment in $activeAssignments) {
        $report += [pscustomobject]@{
            PrincipalId      = $assignment.PrincipalId
            RoleDefinitionId = $assignment.RoleDefinitionId
            AssignmentType   = 'Active'
            StartDateTime    = $assignment.StartDateTime
            EndDateTime      = $assignment.EndDateTime
        }
    }
    foreach ($assignment in $eligibleAssignments) {
        $report += [pscustomobject]@{
            PrincipalId      = $assignment.PrincipalId
            RoleDefinitionId = $assignment.RoleDefinitionId
            AssignmentType   = 'Eligible'
            StartDateTime    = $assignment.StartDateTime
            EndDateTime      = $assignment.EndDateTime
        }
    }

    $report | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Privileged role report failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

External Sharing ReportExternal Sharing Report

Exportiert externe Sharing-Ereignisse für SharePoint und OneDrive aus dem Unified Audit Log.Exports external sharing events for SharePoint and OneDrive from the unified audit log.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [int]$Days = 30,
    [string]$OutputPath = ".\external-sharing-report.csv"
)

try {
    # Search unified audit log for SharePoint and OneDrive sharing events
    Import-Module ExchangeOnlineManagement -ErrorAction Stop
    Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop

    $startDate = (Get-Date).AddDays(-$Days)
    $operations = 'SharingInvitationCreated','SharingSet','AnonymousLinkCreated','SecureLinkCreated'
    $events = Search-UnifiedAuditLog -StartDate $startDate -EndDate (Get-Date) -Operations $operations -ResultSize 5000

    $report = $events | ForEach-Object {
        $data = $_.AuditData | ConvertFrom-Json
        [pscustomobject]@{
            CreationDate = $_.CreationDate
            Operation    = $_.Operations
            UserId       = $_.UserIds
            SiteUrl      = $data.SiteUrl
            ObjectId     = $data.ObjectId
            TargetUser   = $data.TargetUserOrGroupName
        }
    }

    $report | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "External sharing report failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
}

MFA Coverage ReportMFA Coverage Report

Berichtet MFA-Abdeckung und registrierte Methoden pro Benutzer.Reports MFA coverage and registered methods per user.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$OutputPath = ".\mfa-coverage-report.csv"
)

try {
    # Use Graph reporting endpoint for MFA registration status
    Import-Module Microsoft.Graph.Reports -ErrorAction Stop
    Connect-MgGraph -Scopes "Reports.Read.All","Directory.Read.All" -NoWelcome -ErrorAction Stop

    $details = Get-MgReportAuthenticationMethodUserRegistrationDetail -All
    $report = $details | Select-Object UserPrincipalName, UserDisplayName, IsMfaRegistered, IsSsprRegistered,
        @{Name='Methods';Expression={ ($_.MethodsRegistered -join '; ') }},
        @{Name='DefaultMethod';Expression={$_.DefaultMfaMethod}},
        @{Name='IsPasswordlessCapable';Expression={$_.IsPasswordlessCapable}}

    $report | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "MFA coverage report failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

Service Principal Credential ReportService Principal Credential Report

Zeigt Zertifikate und Secrets aller Service Principals inklusive Ablaufdaten.Shows certificates and secrets for all service principals including expiry dates.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$OutputPath = ".\service-principal-credential-report.csv"
)

try {
    # Enumerate service principal credentials and normalize expiry output
    Import-Module Microsoft.Graph.Applications -ErrorAction Stop
    Connect-MgGraph -Scopes "Application.Read.All" -NoWelcome -ErrorAction Stop

    $servicePrincipals = Get-MgServicePrincipal -All -Property Id,AppId,DisplayName,PasswordCredentials,KeyCredentials
    $report = foreach ($sp in $servicePrincipals) {
        foreach ($secret in $sp.PasswordCredentials) {
            [pscustomobject]@{
                DisplayName   = $sp.DisplayName
                AppId         = $sp.AppId
                CredentialType= 'Secret'
                CredentialName= $secret.DisplayName
                EndDateTime   = $secret.EndDateTime
            }
        }
        foreach ($cert in $sp.KeyCredentials) {
            [pscustomobject]@{
                DisplayName   = $sp.DisplayName
                AppId         = $sp.AppId
                CredentialType= 'Certificate'
                CredentialName= $cert.DisplayName
                EndDateTime   = $cert.EndDateTime
            }
        }
    }

    $report | Sort-Object EndDateTime | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Service principal credential report failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

Entra Connect Sync StatusEntra Connect Sync Status

Prüft lokal auf dem Sync-Server den Scheduler, letzte Läufe und bekannte Fehler.Checks the scheduler, recent runs, and known errors locally on the sync server.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$OutputPath = ".\entra-connect-sync-status.csv"
)

try {
    # Run directly on the Entra Connect server
    Import-Module ADSync -ErrorAction Stop

    $scheduler = Get-ADSyncScheduler
    $runHistory = Get-ADSyncRunProfileResult -ConnectorId * -NumberRequested 20

    $report = @(
        [pscustomobject]@{ Metric = 'SyncCycleEnabled'; Value = $scheduler.SyncCycleEnabled },
        [pscustomobject]@{ Metric = 'NextSyncCyclePolicyType'; Value = $scheduler.NextSyncCyclePolicyType },
        [pscustomobject]@{ Metric = 'NextSyncCycleStartTime'; Value = $scheduler.NextSyncCycleStartTimeInUTC },
        [pscustomobject]@{ Metric = 'PurgeRunHistoryInterval'; Value = $scheduler.PurgeRunHistoryInterval },
        [pscustomobject]@{ Metric = 'LastSuccessfulRun'; Value = ($runHistory | Where-Object Result -eq 'success' | Select-Object -First 1).RunEndTime }
    )

    $report | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Entra Connect sync status failed: $($_.Exception.Message)"
    throw
}

Compliance Policy Assignment ReportCompliance Policy Assignment Report

Zeigt Intune-Compliance-Policies samt zugewiesenen Gruppen.Shows Intune compliance policies together with assigned groups.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$OutputPath = ".\compliance-policy-assignments.csv"
)

try {
    # Read Intune compliance policies and their assignments
    Import-Module Microsoft.Graph.DeviceManagement -ErrorAction Stop
    Connect-MgGraph -Scopes "DeviceManagementConfiguration.Read.All","Group.Read.All" -NoWelcome -ErrorAction Stop

    $policies = Get-MgDeviceManagementDeviceCompliancePolicy -All
    $report = foreach ($policy in $policies) {
        foreach ($assignment in $policy.Assignments) {
            [pscustomobject]@{
                PolicyName = $policy.DisplayName
                PolicyId   = $policy.Id
                Assignment = $assignment.Target.AdditionalProperties.'@odata.type'
                GroupId    = $assignment.Target.GroupId
            }
        }
    }

    $report | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Compliance policy assignment report failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

Defender for Office 365 Config AuditDefender for Office 365 Config Audit

Prüft Safe Links, Safe Attachments und Anti-Phishing Basiskonfigurationen.Checks Safe Links, Safe Attachments, and anti-phishing baseline settings.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$OutputPath = ".\defender-o365-config-audit.csv"
)

try {
    # Collect baseline policy state from Exchange Online
    Import-Module ExchangeOnlineManagement -ErrorAction Stop
    Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop

    $report = @()
    Get-SafeLinksPolicy | ForEach-Object {
        $report += [pscustomobject]@{ Workload = 'SafeLinks'; Name = $_.Name; Enabled = $_.EnableSafeLinksForEmail; Detail = "TrackClicks=$($_.TrackClicks)" }
    }
    Get-SafeAttachmentPolicy | ForEach-Object {
        $report += [pscustomobject]@{ Workload = 'SafeAttachments'; Name = $_.Name; Enabled = $true; Detail = "Action=$($_.Action)" }
    }
    Get-AntiPhishPolicy | ForEach-Object {
        $report += [pscustomobject]@{ Workload = 'AntiPhish'; Name = $_.Name; Enabled = $true; Detail = "SpoofIntelligence=$($_.EnableSpoofIntelligence)" }
    }

    $report | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Defender config audit failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
}

DNS Record ValidatorDNS Record Validator

Validiert SPF, DKIM, DMARC, MX und Autodiscover für alle akzeptierten Domains.Validates SPF, DKIM, DMARC, MX, and Autodiscover for all accepted domains.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$OutputPath = ".\dns-validation-report.csv"
)

try {
    # Pull accepted domains and resolve key M365 records
    Import-Module ExchangeOnlineManagement -ErrorAction Stop
    Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop

    $domains = Get-AcceptedDomain | Select-Object -ExpandProperty DomainName
    $report = foreach ($domain in $domains) {
        [pscustomobject]@{
            Domain        = $domain
            SPF           = (Resolve-DnsName -Name $domain -Type TXT -ErrorAction SilentlyContinue | Where-Object Strings -match 'v=spf1').Strings -join ' '
            MX            = (Resolve-DnsName -Name $domain -Type MX -ErrorAction SilentlyContinue | Select-Object -ExpandProperty NameExchange -First 1)
            DMARC         = (Resolve-DnsName -Name "_dmarc.$domain" -Type TXT -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Strings -First 1) -join ' '
            DKIMSelector1 = (Resolve-DnsName -Name "selector1._domainkey.$domain" -Type CNAME -ErrorAction SilentlyContinue | Select-Object -ExpandProperty NameHost -First 1)
            Autodiscover  = (Resolve-DnsName -Name "autodiscover.$domain" -Type CNAME -ErrorAction SilentlyContinue | Select-Object -ExpandProperty NameHost -First 1)
        }
    }

    $report | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "DNS validation failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
}

User Sign-in Activity Heatmap DataUser Sign-in Activity Heatmap Data

Exportiert Anmeldedaten pro Wochentag und Stunde für Visualisierungen.Exports sign-in data by weekday and hour for visualizations.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [int]$Days = 30,
    [string]$OutputPath = ".\signin-heatmap-data.csv"
)

try {
    # Summarize sign-ins by weekday and hour
    Import-Module Microsoft.Graph.Identity.SignIns -ErrorAction Stop
    Connect-MgGraph -Scopes "AuditLog.Read.All" -NoWelcome -ErrorAction Stop

    $since = (Get-Date).AddDays(-$Days).ToUniversalTime().ToString('s') + 'Z'
    $signIns = Get-MgAuditLogSignIn -All -Filter "createdDateTime ge $since"

    $heatmap = $signIns | Group-Object { "{0}-{1}" -f $_.CreatedDateTime.DayOfWeek, $_.CreatedDateTime.Hour } | ForEach-Object {
        $parts = $_.Name.Split('-')
        [pscustomobject]@{
            Weekday     = $parts[0]
            Hour        = [int]$parts[1]
            SignInCount = $_.Count
        }
    }

    $heatmap | Sort-Object Weekday, Hour | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Sign-in heatmap export failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

Bulk Password Reset with Temporary Access PassBulk Password Reset with Temporary Access Pass

Setzt Kennwörter zurück und erstellt Temporary Access Passes für importierte Benutzer.Resets passwords and creates Temporary Access Passes for imported users.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [Parameter(Mandatory)]
    [string]$InputCsv,
    [string]$OutputPath = ".\bulk-password-reset-results.csv"
)

try {
    # Reset password and create a Temporary Access Pass per user
    Import-Module Microsoft.Graph.Users -ErrorAction Stop
    Connect-MgGraph -Scopes "User.ReadWrite.All","UserAuthenticationMethod.ReadWrite.All" -NoWelcome -ErrorAction Stop

    $users = Import-Csv $InputCsv -ErrorAction Stop
    $report = foreach ($user in $users) {
        Update-MgUser -UserId $user.UserPrincipalName -PasswordProfile @{ ForceChangePasswordNextSignIn = $true; Password = $user.NewPassword } -ErrorAction Stop
        $tap = New-MgUserAuthenticationTemporaryAccessPassMethod -UserId $user.UserPrincipalName -BodyParameter @{ isUsableOnce = $true; lifetimeInMinutes = 60 } -ErrorAction Stop

        [pscustomobject]@{
            UserPrincipalName     = $user.UserPrincipalName
            PasswordReset         = $true
            TemporaryAccessPass   = $tap.TemporaryAccessPass
            TemporaryAccessPassId = $tap.Id
        }
    }

    $report | Export-Csv $OutputPath -NoTypeInformation -Encoding UTF8
}
catch {
    Write-Error "Bulk password reset failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}

Complete Tenant Security Posture ReportComplete Tenant Security Posture Report

Kombiniert MFA-Status, CA-Richtlinien, Admin-Rollen, Gastanzahl und inaktive Konten in einem Bericht.Combines MFA status, CA policies, admin roles, guest count, and inactive accounts in one report.

VoraussetzungenRequirements
PowerShellPowerShell

param(
    [string]$CsvOutputPath = ".\tenant-security-posture-summary.csv",
    [string]$JsonOutputPath = ".\tenant-security-posture-details.json"
)

try {
    # Gather security posture metrics from multiple Graph workloads
    Import-Module Microsoft.Graph.Users, Microsoft.Graph.Reports, Microsoft.Graph.Identity.SignIns, Microsoft.Graph.Identity.Governance -ErrorAction Stop
    Connect-MgGraph -Scopes "User.Read.All","Reports.Read.All","AuditLog.Read.All","Policy.Read.All","RoleManagement.Read.All" -NoWelcome -ErrorAction Stop

    $users = Get-MgUser -All -Property Id,UserType,AccountEnabled,SignInActivity
    $mfa = Get-MgReportAuthenticationMethodUserRegistrationDetail -All
    $caPolicies = Get-MgIdentityConditionalAccessPolicy -All
    $activeRoles = Get-MgRoleManagementDirectoryRoleAssignmentSchedule -All

    $summary = @(
        [pscustomobject]@{ Metric = 'TotalUsers'; Value = $users.Count },
        [pscustomobject]@{ Metric = 'Guests'; Value = ($users | Where-Object UserType -eq 'Guest').Count },
        [pscustomobject]@{ Metric = 'Inactive90Days'; Value = ($users | Where-Object { $_.SignInActivity.LastSuccessfulSignInDateTime -and ([datetime]$_.SignInActivity.LastSuccessfulSignInDateTime) -lt (Get-Date).AddDays(-90) }).Count },
        [pscustomobject]@{ Metric = 'MfaRegistered'; Value = ($mfa | Where-Object IsMfaRegistered).Count },
        [pscustomobject]@{ Metric = 'ConditionalAccessPolicies'; Value = $caPolicies.Count },
        [pscustomobject]@{ Metric = 'PrivilegedAssignments'; Value = $activeRoles.Count }
    )

    $summary | Export-Csv $CsvOutputPath -NoTypeInformation -Encoding UTF8

    $details = [pscustomobject]@{
        GeneratedAt = Get-Date
        Summary     = $summary
        Policies    = $caPolicies | Select-Object DisplayName, State, CreatedDateTime, ModifiedDateTime
    }
    $details | ConvertTo-Json -Depth 10 | Set-Content -Path $JsonOutputPath -Encoding UTF8
}
catch {
    Write-Error "Tenant security posture report failed: $($_.Exception.Message)"
    throw
}
finally {
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
}