:

    Intune-Zuweisungsreport

    Intune bietet keine einfache Möglichkeit, alle Gruppenzuweisungen zentral auszulesen. Mit dem im Video gezeigten Skript könnt ihr zwei Berichte erstellen: einen Ressourcen-Report, der alle Intune-Objekte und deren Zuweisungen zeigt, sowie einen Gruppen-Report, der alle Gruppen mit deren zugewiesenen Ressourcen liefert.



    Benötigst du Unterstützung?

    Melde dich bei uns!



    Hier findest du das im Video genutzte Script. Bitte beachte die Nutzungshinweise im Header des Scripts. Es wird ohne Gewähr bereitgestellt und auf eigene Gefahr genutzt.

    Get-IntuneAssignmentReport.ps1

    #Requires -Modules Microsoft.Graph.Authentication
    
    <#
    .SYNOPSIS
        Intune Assignment Report - Analyse aller Intune-Ressourcen und deren Gruppenzuweisungen
    
    .DESCRIPTION
        Das Intune Assignment Report-Tool analysiert sämtliche Intune-Ressourcen und deren 
        Gruppenzuweisungen über die Microsoft Graph API. Es erzeugt professionelle HTML-Reports 
        aus zwei Perspektiven, um einen vollständigen Überblick über die Zuweisungsstruktur 
        des Tenants zu erhalten.
    
        Funktionen:
        [1] Ressourcen-Sicht - Alle Intune-Objekte mit ihren zugewiesenen Gruppen
            Listet pro Kategorie sämtliche Ressourcen auf, zeigt Include- und Exclude-Gruppen
            sowie spezielle Zuweisungen (Alle Benutzer / Alle Geräte) an.
    
        [2] Gruppen-Sicht - Alle Gruppen mit den ihnen zugewiesenen Intune-Objekten
            Invertiertes Mapping: Pro Entra ID-Gruppe werden alle zugewiesenen Intune-Ressourcen
            aufgelistet, gruppiert nach Kategorie.
    
        [3] Beide Reports erstellen
    
        Abgedeckte Intune-Ressourcen:
            - Configuration Policies (Device Configurations, Settings Catalog, Administrative Templates)
            - Apps (Mobile Apps inkl. Win32, MSI, Store, LOB)
            - Compliance Policies
            - Windows Update Policies (Update Rings, Driver, Feature, Quality, Expedited Updates)
            - Enrollment Profiles (Windows Autopilot, iOS, Android)
            - Scripts (PowerShell, Remediation / Device Health, macOS Shell)
    
        Das Tool arbeitet mit dem Microsoft Graph PowerShell SDK und nutzt die Microsoft Graph 
        REST API (überwiegend Beta-Endpunkte, da für vollständige Abdeckung erforderlich). 
        Sämtliche Endpunkte werden mit automatischem Paging abgefragt und Gruppennamen über 
        einen vorab geladenen Cache aufgelöst.
    
    .NOTES
        Dateiname:      Get-IntuneAssignmentReport.ps1
        Autor:          itelio GmbH
        Voraussetzungen:
            - PowerShell 5.1 oder höher
            - Microsoft.Graph.Authentication Modul (Microsoft Graph PowerShell SDK)
            - Microsoft Graph API-Berechtigungen (siehe unten)
    
        WICHTIG - Ausführungsumgebung:
            Das Skript MUSS aus einer normalen PowerShell-Konsole ausgeführt werden.
            PowerShell ISE wird NICHT unterstützt, da die Authentifizierung mit Web Account 
            Manager (WAM) in der ISE nicht funktioniert. Verwenden Sie Windows Terminal, 
            PowerShell 7, oder die klassische PowerShell-Konsole.
    
        Erforderliche Microsoft Graph API-Berechtigungen (Delegated Permissions):
            - DeviceManagementManagedDevices.Read.All
              Benötigt für: Grundlegende Geräte-Informationen
    
            - DeviceManagementConfiguration.Read.All
              Benötigt für: Configuration Policies, Compliance Policies, 
                            Windows Update Policies und deren Assignments
    
            - DeviceManagementApps.Read.All
              Benötigt für: Mobile Apps und deren Assignments
    
            - DeviceManagementServiceConfig.Read.All
              Benötigt für: Enrollment Profiles und deren Assignments
    
            - DeviceManagementScripts.Read.All
              Benötigt für: PowerShell Scripts, Remediation Scripts, 
                            macOS Shell Scripts und deren Assignments
    
            - Group.Read.All
              Benötigt für: Auflösung der Gruppen-IDs in Anzeigenamen
    
        Erforderliche Admin-Rolle zum Erteilen der Berechtigungen:
            Zum Consent der oben genannten Graph API-Berechtigungen wird eine der 
            folgenden Entra ID-Rollen benötigt:
            - Global Administrator (empfohlen für initiales Setup)
            - Intune Administrator (ausreichend für alle Device Management Scopes)
            
        Beim ersten Aufruf fordert das Skript interaktiv die Anmeldung und den Consent 
        für alle benötigten Berechtigungen an. Nachfolgende Aufrufe in derselben Session 
        nutzen die bestehende Authentifizierung.
    
    .EXAMPLE
        .\Get-IntuneAssignmentReport.ps1
    
        Startet das interaktive Menü. Wählen Sie die gewünschte Report-Perspektive durch 
        Eingabe der entsprechenden Nummer.
    
    .EXAMPLE
        .\Get-IntuneAssignmentReport.ps1 -ReportMode Beide -OutputPath "C:\Reports"
    
        Erstellt beide Reports ohne interaktives Menü und speichert sie im angegebenen Verzeichnis.
    
    .LINK
        https://learn.microsoft.com/en-us/graph/api/resources/intune-graph-overview
        https://learn.microsoft.com/en-us/powershell/microsoftgraph/
    
    .DISCLAIMER
        HAFTUNGSAUSSCHLUSS:
        Dieses Skript wird "wie besehen" ohne jegliche Gewährleistung bereitgestellt. Die Nutzung 
        erfolgt auf eigene Gefahr. Die itelio GmbH übernimmt keine Haftung für Schäden, Datenverlust 
        oder andere Probleme, die durch die Verwendung dieses Skripts entstehen können.
    
        Es wird empfohlen, das Skript zunächst in einer Testumgebung auszuführen und die erzeugten 
        Reports auf Plausibilität zu prüfen.
    #>
    
    [CmdletBinding()]
    param(
        # Ausgabeverzeichnis für die HTML-Reports
        [string]$OutputPath,
    
        # Report-Modus: 'Ressourcen', 'Gruppen' oder 'Beide'
        [ValidateSet('Ressourcen', 'Gruppen', 'Beide')]
        [string]$ReportMode
    )
    
    # OutputPath wird nach der Menüauswahl interaktiv oder per Default bestimmt (siehe unten).
    
    # ============================================================================
    # Region: Konfiguration und Hilfsfunktionen
    # ============================================================================
    
    $ErrorActionPreference = 'Continue'
    
    # Farbschema (blau/weiß)
    $script:HtmlColors = @{
        Primary       = '#0063B1'
        PrimaryDark   = '#004A85'
        PrimaryLight  = '#E8F1FA'
        Accent        = '#00A4EF'
        White         = '#FFFFFF'
        TextDark      = '#1A1A1A'
        TextMuted     = '#6B7280'
        Border        = '#D1D5DB'
        RowAlt        = '#F9FAFB'
        Success       = '#10B981'
        Warning       = '#F59E0B'
        Danger        = '#EF4444'
        TagInclude    = '#DBEAFE'
        TagExclude    = '#FEE2E2'
        TagAll        = '#D1FAE5'
    }
    
    function Write-Status {
        <#
        .SYNOPSIS
            Gibt eine formatierte Statusmeldung auf der Konsole aus.
        #>
        param(
            [string]$Message,
            [ValidateSet('Info', 'Success', 'Warning', 'Error')]
            [string]$Type = 'Info'
        )
    
        $colors = @{
            Info    = 'Cyan'
            Success = 'Green'
            Warning = 'Yellow'
            Error   = 'Red'
        }
        $prefixes = @{
            Info    = '[i]'
            Success = '[✓]'
            Warning = '[!]'
            Error   = '[✗]'
        }
    
        Write-Host "$($prefixes[$Type]) $Message" -ForegroundColor $colors[$Type]
    }
    
    function Invoke-MgGraphRequestPaged {
        <#
        .SYNOPSIS
            Führt einen Graph-API-Request mit automatischem Paging durch.
        .DESCRIPTION
            Iteriert über alle @odata.nextLink-Seiten und gibt die kombinierten
            .value-Einträge zurück.
        #>
        param(
            [Parameter(Mandatory)]
            [string]$Uri,
    
            [string]$Description = ''
        )
    
        $allItems = [System.Collections.Generic.List[object]]::new()
    
        try {
            $response = Invoke-MgGraphRequest -Method GET -Uri $Uri -ErrorAction Stop
            
            if ($response.value) {
                $allItems.AddRange([object[]]$response.value)
            }
    
            # Paging: Solange @odata.nextLink vorhanden, weitere Seiten abrufen
            while ($response.'@odata.nextLink') {
                $response = Invoke-MgGraphRequest -Method GET -Uri $response.'@odata.nextLink' -ErrorAction Stop
                if ($response.value) {
                    $allItems.AddRange([object[]]$response.value)
                }
            }
        }
        catch {
            $descText = ''
            if ($Description) { $descText = " ($Description)" }
            Write-Status "Fehler beim Abruf: $Uri$descText - $($_.Exception.Message)" -Type Error
        }
    
        return $allItems
    }
    
    function Get-ResourceAssignments {
        <#
        .SYNOPSIS
            Ruft die Assignments einer einzelnen Intune-Ressource ab.
        .DESCRIPTION
            Gibt eine Liste von Assignment-Objekten zurück mit:
            - GroupId, GroupName (wird später aufgelöst)
            - AssignmentType (Include / Exclude / AllUsers / AllDevices)
        #>
        param(
            [Parameter(Mandatory)]
            [string]$AssignmentUri
        )
    
        $assignments = [System.Collections.Generic.List[PSCustomObject]]::new()
    
        try {
            $response = Invoke-MgGraphRequest -Method GET -Uri $AssignmentUri -ErrorAction Stop
            
            foreach ($assignment in $response.value) {
                $target = $assignment.target
                if (-not $target) { continue }
    
                $type = $target.'@odata.type'
                $groupId = $target.groupId
    
                $assignmentType = switch ($type) {
                    '#microsoft.graph.groupAssignmentTarget'          { 'Include' }
                    '#microsoft.graph.exclusionGroupAssignmentTarget'  { 'Exclude' }
                    '#microsoft.graph.allLicensedUsersAssignmentTarget' { 'AllUsers' }
                    '#microsoft.graph.allDevicesAssignmentTarget'       { 'AllDevices' }
                    default { 'Unknown' }
                }
    
                $assignments.Add([PSCustomObject]@{
                    GroupId        = $groupId
                    AssignmentType = $assignmentType
                })
            }
        }
        catch {
            # Manche Ressourcen haben keine Assignments oder der Endpunkt existiert nicht
            Write-Verbose "Assignments nicht abrufbar: $AssignmentUri - $($_.Exception.Message)"
        }
    
        return $assignments
    }
    
    function Get-AllGroupsLookup {
        <#
        .SYNOPSIS
            Lädt alle Entra ID-Gruppen und erstellt ein ID->Name Lookup-Dictionary.
        #>
        Write-Status "Lade Entra ID-Gruppen für Namensauflösung..."
    
        $groups = Invoke-MgGraphRequestPaged -Uri 'https://graph.microsoft.com/v1.0/groups?$select=id,displayName&$top=999' -Description 'Gruppen'
        
        $lookup = @{}
        foreach ($group in $groups) {
            $lookup[$group.id] = $group.displayName
        }
    
        Write-Status "$($lookup.Count) Gruppen geladen." -Type Success
        return $lookup
    }
    
    function Resolve-GroupName {
        <#
        .SYNOPSIS
            Löst eine GroupId in den Anzeigenamen auf.
            Fällt auf die ID zurück, wenn nicht im Cache.
        #>
        param(
            [string]$GroupId,
            [hashtable]$GroupLookup
        )
    
        if ([string]::IsNullOrEmpty($GroupId)) { return $null }
        
        if ($GroupLookup.ContainsKey($GroupId)) {
            return $GroupLookup[$GroupId]
        }
    
        # Einzelabruf als Fallback
        try {
            $group = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups/$GroupId`?`$select=displayName" -ErrorAction Stop
            $name = $group.displayName
            $GroupLookup[$GroupId] = $name
            return $name
        }
        catch {
            return $GroupId  # ID als Fallback
        }
    }
    
    # ============================================================================
    # Region: Datensammlung - Intune-Ressourcen
    # ============================================================================
    
    function Get-IntuneResources {
        <#
        .SYNOPSIS
            Sammelt alle Intune-Ressourcen inkl. Assignments aus sämtlichen Kategorien.
        .OUTPUTS
            Array von PSCustomObjects mit: Category, SubCategory, Name, Type, Id,
            CreatedDateTime, LastModifiedDateTime, Assignments
        #>
        param(
            [hashtable]$GroupLookup
        )
    
        $allResources = [System.Collections.Generic.List[PSCustomObject]]::new()
    
        # -----------------------------------------------------------------
        # Kategorie-Definitionen: Jede Kategorie hat einen oder mehrere Endpunkte
        # -----------------------------------------------------------------
        $categories = @(
            # --- Configuration Policies ---
            @{
                Category    = 'Configuration Policies'
                SubCategory = 'Device Configurations'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = '@odata.type'
            },
            @{
                Category    = 'Configuration Policies'
                SubCategory = 'Settings Catalog / Endpoint Security'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies(''{id}'')/assignments'
                NameProp    = 'name'
                TypeProp    = 'platforms'
            },
            @{
                Category    = 'Configuration Policies'
                SubCategory = 'Administrative Templates'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
            },
    
            # --- Apps ---
            @{
                Category    = 'Apps'
                SubCategory = 'Mobile Apps'
                ListUri     = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = '@odata.type'
            },
    
            # --- Compliance Policies ---
            @{
                Category    = 'Compliance Policies'
                SubCategory = 'Device Compliance'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = '@odata.type'
            },
    
            # --- Windows Update Policies ---
            @{
                Category    = 'Windows Update Policies'
                SubCategory = 'Update Rings'
                ListUri     = "https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations?`$top=100&`$filter=isof('microsoft.graph.windowsUpdateForBusinessConfiguration')"
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = '@odata.type'
            },
            @{
                Category    = 'Windows Update Policies'
                SubCategory = 'Driver Update Profiles'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
            },
            @{
                Category    = 'Windows Update Policies'
                SubCategory = 'Feature Update Profiles'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/windowsFeatureUpdateProfiles?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/windowsFeatureUpdateProfiles(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
            },
            @{
                Category    = 'Windows Update Policies'
                SubCategory = 'Quality Update Profiles'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/windowsQualityUpdateProfiles?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/windowsQualityUpdateProfiles(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
            },
            @{
                Category    = 'Windows Update Policies'
                SubCategory = 'Expedited Quality Updates'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/windowsQualityUpdatePolicies?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/windowsQualityUpdatePolicies(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
            },
    
            # --- Enrollment Profiles ---
            @{
                Category    = 'Enrollment Profiles'
                SubCategory = 'Windows Autopilot'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
            },
            @{
                Category    = 'Enrollment Profiles'
                SubCategory = 'iOS Enrollment'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/appleUserInitiatedEnrollmentProfiles?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/appleUserInitiatedEnrollmentProfiles(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
            },
            @{
                Category    = 'Enrollment Profiles'
                SubCategory = 'Android Enrollment (Device Owner)'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/androidDeviceOwnerEnrollmentProfiles?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/androidDeviceOwnerEnrollmentProfiles(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
                MergeGroup  = 'AndroidEnrollment'
            },
            @{
                Category    = 'Enrollment Profiles'
                SubCategory = 'Android Enrollment (Work Profile)'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/androidForWorkEnrollmentProfiles?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/androidForWorkEnrollmentProfiles(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
                MergeGroup  = 'AndroidEnrollment'
            },
    
            # --- Scripts ---
            @{
                Category    = 'Scripts'
                SubCategory = 'PowerShell Scripts'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
            },
            @{
                Category    = 'Scripts'
                SubCategory = 'Remediation Scripts'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
            },
            @{
                Category    = 'Scripts'
                SubCategory = 'macOS Shell Scripts'
                ListUri     = 'https://graph.microsoft.com/beta/deviceManagement/deviceShellScripts?$top=100'
                AssignUri   = 'https://graph.microsoft.com/beta/deviceManagement/deviceShellScripts(''{id}'')/assignments'
                NameProp    = 'displayName'
                TypeProp    = $null
            }
        )
    
        # Deduplizierungs-Tracker für Android Enrollment
        $androidIds = [System.Collections.Generic.HashSet[string]]::new()
    
        $totalCategories = $categories.Count
        $currentCategory = 0
    
        foreach ($cat in $categories) {
            $currentCategory++
            $pctCategory = [math]::Round(($currentCategory / $totalCategories) * 100)
            Write-Progress -Activity "Intune-Daten sammeln" -Status "$($cat.SubCategory)" -PercentComplete $pctCategory
    
            Write-Status "Lade $($cat.SubCategory)..."
            $items = Invoke-MgGraphRequestPaged -Uri $cat.ListUri -Description $cat.SubCategory
    
            if (-not $items -or $items.Count -eq 0) {
                Write-Status "  Keine Einträge gefunden für $($cat.SubCategory)." -Type Warning
                continue
            }
    
            Write-Status "  $($items.Count) Einträge gefunden. Lade Assignments..."
    
            $itemCount = 0
            foreach ($item in $items) {
                $itemCount++
                $itemId = $item.id
    
                # Deduplizierung für Android Enrollment
                if ($cat.MergeGroup -eq 'AndroidEnrollment') {
                    if ($androidIds.Contains($itemId)) { continue }
                    [void]$androidIds.Add($itemId)
                }
    
                # Assignments abrufen
                $assignUri = $cat.AssignUri -replace '\{id\}', $itemId
                $assignments = Get-ResourceAssignments -AssignmentUri $assignUri
    
                # Assignments mit Gruppennamen anreichern
                $resolvedAssignments = foreach ($a in $assignments) {
                    $groupName = switch ($a.AssignmentType) {
                        'AllUsers'   { 'Alle lizenzierten Benutzer' }
                        'AllDevices' { 'Alle Geräte' }
                        default      { Resolve-GroupName -GroupId $a.GroupId -GroupLookup $GroupLookup }
                    }
    
                    [PSCustomObject]@{
                        GroupId        = $a.GroupId
                        GroupName      = $groupName
                        AssignmentType = $a.AssignmentType
                    }
                }
    
                # Typ-Information extrahieren
                if ($cat.TypeProp -and $item.($cat.TypeProp)) {
                    $rawType = $item.($cat.TypeProp)
                    # OData-Typen kürzen: #microsoft.graph.win32LobApp -> win32LobApp
                    if ($rawType -match '\.(\w+)$') { $typeValue = $Matches[1] } else { $typeValue = $rawType }
                } else {
                    $typeValue = $cat.SubCategory
                }
    
                $resourceName = $item.($cat.NameProp)
                if ([string]::IsNullOrEmpty($resourceName)) { $resourceName = "(Kein Name - ID: $itemId)" }
    
                $allResources.Add([PSCustomObject]@{
                    Category             = $cat.Category
                    SubCategory          = $cat.SubCategory
                    Name                 = $resourceName
                    Type                 = $typeValue
                    Id                   = $itemId
                    CreatedDateTime      = $item.createdDateTime
                    LastModifiedDateTime = $item.lastModifiedDateTime
                    Assignments          = @($resolvedAssignments)
                })
    
                # Progress innerhalb der Kategorie
                if ($itemCount % 10 -eq 0) {
                    Write-Progress -Activity "Intune-Daten sammeln" -Status "$($cat.SubCategory) - $itemCount/$($items.Count)" -PercentComplete $pctCategory
                }
            }
    
            Write-Status "  $($cat.SubCategory) abgeschlossen: $itemCount Ressourcen verarbeitet." -Type Success
        }
    
        Write-Progress -Activity "Intune-Daten sammeln" -Completed
        return $allResources
    }
    
    # ============================================================================
    # Region: HTML-Report-Generierung
    # ============================================================================
    
    function Get-HtmlHead {
        <#
        .SYNOPSIS
            Erzeugt den HTML-Header mit CSS.
        #>
        param([string]$Title)
    
        $c = $script:HtmlColors
    
        return @"
    <!DOCTYPE html>
    <html lang="de">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>$Title</title>
        <style>
            * { margin: 0; padding: 0; box-sizing: border-box; }
    
            body {
                font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
                background: #F3F4F6;
                color: $($c.TextDark);
                line-height: 1.5;
            }
    
            /* Header */
            .header {
                background: linear-gradient(135deg, $($c.Primary), $($c.PrimaryDark));
                color: $($c.White);
                padding: 2rem 2.5rem;
                box-shadow: 0 2px 8px rgba(0,0,0,0.15);
            }
            .header h1 {
                font-size: 1.75rem;
                font-weight: 600;
                margin-bottom: 0.25rem;
            }
            .header .subtitle {
                font-size: 0.9rem;
                opacity: 0.85;
            }
            .header .meta {
                margin-top: 0.75rem;
                font-size: 0.8rem;
                opacity: 0.7;
            }
    
            /* Container */
            .container {
                max-width: 1400px;
                margin: 1.5rem auto;
                padding: 0 1.5rem;
            }
    
            /* Summary Cards */
            .summary-grid {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
                gap: 1rem;
                margin-bottom: 2rem;
            }
            .summary-card {
                background: $($c.White);
                border-radius: 8px;
                padding: 1.25rem;
                box-shadow: 0 1px 3px rgba(0,0,0,0.08);
                border-left: 4px solid $($c.Primary);
            }
            .summary-card .label {
                font-size: 0.8rem;
                color: $($c.TextMuted);
                text-transform: uppercase;
                letter-spacing: 0.05em;
            }
            .summary-card .value {
                font-size: 1.75rem;
                font-weight: 700;
                color: $($c.Primary);
                margin-top: 0.25rem;
            }
    
            /* Sections */
            .section {
                background: $($c.White);
                border-radius: 8px;
                margin-bottom: 1.5rem;
                box-shadow: 0 1px 3px rgba(0,0,0,0.08);
                overflow: hidden;
            }
            .section-header {
                background: $($c.PrimaryLight);
                padding: 1rem 1.5rem;
                border-bottom: 2px solid $($c.Primary);
                cursor: pointer;
                user-select: none;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }
            .section-header:hover {
                background: #D6E8F6;
            }
            .section-header h2 {
                font-size: 1.1rem;
                font-weight: 600;
                color: $($c.PrimaryDark);
            }
            .section-header .badge {
                background: $($c.Primary);
                color: $($c.White);
                padding: 0.2rem 0.6rem;
                border-radius: 12px;
                font-size: 0.75rem;
                font-weight: 600;
            }
            .section-content {
                padding: 0;
            }
    
            /* Table */
            table {
                width: 100%;
                border-collapse: collapse;
                font-size: 0.85rem;
            }
            thead th {
                background: $($c.PrimaryDark);
                color: $($c.White);
                padding: 0.65rem 1rem;
                text-align: left;
                font-weight: 600;
                white-space: nowrap;
                position: sticky;
                top: 0;
            }
            tbody tr {
                border-bottom: 1px solid $($c.Border);
            }
            tbody tr:nth-child(even) {
                background: $($c.RowAlt);
            }
            tbody tr:hover {
                background: $($c.PrimaryLight);
            }
            tbody td {
                padding: 0.55rem 1rem;
                vertical-align: top;
            }
    
            /* Tags für Gruppen */
            .tag {
                display: inline-block;
                padding: 0.15rem 0.5rem;
                border-radius: 4px;
                font-size: 0.75rem;
                margin: 0.1rem 0.15rem;
                white-space: nowrap;
            }
            .tag-include {
                background: $($c.TagInclude);
                color: #1E40AF;
                border: 1px solid #93C5FD;
            }
            .tag-exclude {
                background: $($c.TagExclude);
                color: #991B1B;
                border: 1px solid #FCA5A5;
            }
            .tag-all {
                background: $($c.TagAll);
                color: #065F46;
                border: 1px solid #6EE7B7;
            }
            .tag-none {
                color: $($c.TextMuted);
                font-style: italic;
            }
    
            /* Gruppen-Sicht spezifisch */
            .group-section {
                margin-bottom: 1rem;
            }
            .group-section .group-header {
                background: $($c.Primary);
                color: $($c.White);
                padding: 0.75rem 1.25rem;
                border-radius: 6px 6px 0 0;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }
            .group-section .group-header h3 {
                font-size: 1rem;
                font-weight: 600;
            }
            .group-resources {
                border: 1px solid $($c.Border);
                border-top: none;
                border-radius: 0 0 6px 6px;
                padding: 0.75rem 1rem;
                background: $($c.White);
            }
            .resource-category {
                margin-bottom: 0.5rem;
            }
            .resource-category .cat-label {
                font-size: 0.75rem;
                font-weight: 600;
                color: $($c.TextMuted);
                text-transform: uppercase;
                letter-spacing: 0.04em;
                margin-bottom: 0.25rem;
            }
            .resource-item {
                padding: 0.3rem 0;
                font-size: 0.85rem;
                display: flex;
                justify-content: space-between;
                border-bottom: 1px solid #F3F4F6;
            }
            .resource-item:last-child { border-bottom: none; }
            .resource-item .res-name { font-weight: 500; }
            .resource-item .res-type { color: $($c.TextMuted); font-size: 0.8rem; }
    
            /* Navigation / TOC */
            .toc {
                background: $($c.White);
                border-radius: 8px;
                padding: 1.25rem 1.5rem;
                margin-bottom: 1.5rem;
                box-shadow: 0 1px 3px rgba(0,0,0,0.08);
            }
            .toc h3 {
                font-size: 0.9rem;
                color: $($c.PrimaryDark);
                margin-bottom: 0.75rem;
            }
            .toc a {
                color: $($c.Primary);
                text-decoration: none;
                font-size: 0.85rem;
                display: inline-block;
                margin: 0.2rem 0.75rem 0.2rem 0;
            }
            .toc a:hover { text-decoration: underline; }
    
            /* Footer */
            .footer {
                text-align: center;
                padding: 2rem;
                color: $($c.TextMuted);
                font-size: 0.75rem;
            }
    
            /* Print */
            @media print {
                body { background: white; }
                .header { box-shadow: none; }
                .section { break-inside: avoid; }
                .section-header { cursor: default; }
            }
        </style>
    </head>
    <body>
    "@
    }
    
    function Get-HtmlFooter {
        return @"
        <div class="footer">
            Erstellt am $(Get-Date -Format 'dd.MM.yyyy HH:mm') Uhr &bull; Intune Assignment Report &bull; itelio
        </div>
    </body>
    </html>
    "@
    }
    
    function Format-AssignmentTags {
        <#
        .SYNOPSIS
            Erzeugt HTML-Tags für die Assignments einer Ressource.
        #>
        param(
            [array]$Assignments
        )
    
        if (-not $Assignments -or $Assignments.Count -eq 0) {
            return '<span class="tag-none">Keine Zuweisung</span>'
        }
    
        $html = ''
        foreach ($a in $Assignments) {
            $class = switch ($a.AssignmentType) {
                'Include'    { 'tag-include' }
                'Exclude'    { 'tag-exclude' }
                'AllUsers'   { 'tag-all' }
                'AllDevices' { 'tag-all' }
                default      { 'tag-include' }
            }
    
            $label = switch ($a.AssignmentType) {
                'Exclude' { "$($a.GroupName) (Exclude)" }
                default   { $a.GroupName }
            }
    
            $html += "<span class=`"tag $class`">$([System.Web.HttpUtility]::HtmlEncode($label))</span>`n"
        }
    
        return $html
    }
    
    function Format-DateTime {
        <#
        .SYNOPSIS
            Formatiert einen ISO 8601 DateTime-String von Graph API in dd.MM.yyyy HH:mm.
        .DESCRIPTION
            Graph API liefert Datumsangaben in ISO 8601 UTC, z.B.:
              - 2026-01-14T15:22:25Z
              - 2026-01-14T15:22:25.1234567Z
              - 2026-02-04T09:14:00.0000000+00:00
            [datetime]::Parse() ist kultursensitiv und vertauscht je nach Locale
            Monat/Tag. Daher wird hier explizit mit InvariantCulture geparst und
            anschließend in Lokalzeit konvertiert.
        #>
        param([string]$DateString)
    
        if ([string]::IsNullOrEmpty($DateString)) { return '–' }
    
        try {
            # DateTimeOffset versteht alle ISO 8601-Varianten kulturunabhängig
            $dto = [System.DateTimeOffset]::Parse(
                $DateString,
                [System.Globalization.CultureInfo]::InvariantCulture,
                [System.Globalization.DateTimeStyles]::None
            )
            # In Lokalzeit des ausführenden Systems konvertieren
            return $dto.LocalDateTime.ToString('dd.MM.yyyy HH:mm')
        }
        catch {
            return $DateString
        }
    }
    
    function New-ResourceReport {
        <#
        .SYNOPSIS
            Erstellt den HTML-Report aus Ressourcen-Perspektive.
        #>
        param(
            [array]$Resources,
            [string]$OutputFile
        )
    
        Write-Status "Erstelle Ressourcen-Report..."
    
        # Gruppierung nach Kategorie
        $grouped = $Resources | Group-Object -Property Category
    
        # Zusammenfassung berechnen
        $totalResources   = $Resources.Count
        $totalAssigned    = ($Resources | Where-Object { $_.Assignments.Count -gt 0 }).Count
        $totalUnassigned  = $totalResources - $totalAssigned
        $totalCategories  = $grouped.Count
        $uniqueGroups     = ($Resources.Assignments | Where-Object { $_.GroupId } | Select-Object -ExpandProperty GroupId -Unique).Count
    
        $sb = [System.Text.StringBuilder]::new()
    
        # HTML Head
        [void]$sb.Append((Get-HtmlHead -Title "Intune Ressourcen-Report"))
    
        # Header
        [void]$sb.Append(@"
        <div class="header">
            <h1>Intune Assignment Report &ndash; Ressourcen-Sicht</h1>
            <div class="subtitle">Übersicht aller Intune-Ressourcen und deren Gruppenzuweisungen</div>
            <div class="meta">Erstellt am $(Get-Date -Format 'dd.MM.yyyy') um $(Get-Date -Format 'HH:mm') Uhr &bull; Tenant: $((Get-MgContext).TenantId)</div>
        </div>
        <div class="container">
    "@)
    
        # Summary Cards
        [void]$sb.Append(@"
            <div class="summary-grid">
                <div class="summary-card"><div class="label">Ressourcen gesamt</div><div class="value">$totalResources</div></div>
                <div class="summary-card"><div class="label">Mit Zuweisung</div><div class="value">$totalAssigned</div></div>
                <div class="summary-card"><div class="label">Ohne Zuweisung</div><div class="value">$totalUnassigned</div></div>
                <div class="summary-card"><div class="label">Kategorien</div><div class="value">$totalCategories</div></div>
                <div class="summary-card"><div class="label">Verwendete Gruppen</div><div class="value">$uniqueGroups</div></div>
            </div>
    "@)
    
        # TOC
        [void]$sb.Append('<div class="toc"><h3>Kategorien</h3>')
        foreach ($group in $grouped) {
            $anchorId = ($group.Name -replace '\s+', '-').ToLower()
            [void]$sb.Append("<a href=`"#$anchorId`">$([System.Web.HttpUtility]::HtmlEncode($group.Name)) ($($group.Count))</a>")
        }
        [void]$sb.Append('</div>')
    
        # Sektionen pro Kategorie
        foreach ($group in $grouped) {
            $anchorId = ($group.Name -replace '\s+', '-').ToLower()
            [void]$sb.Append(@"
            <div class="section" id="$anchorId">
                <div class="section-header">
                    <h2>$([System.Web.HttpUtility]::HtmlEncode($group.Name))</h2>
                    <span class="badge">$($group.Count) Einträge</span>
                </div>
                <div class="section-content">
                    <table>
                        <thead>
                            <tr>
                                <th>Name</th>
                                <th>Typ / Plattform</th>
                                <th>Unterkategorie</th>
                                <th>Zugewiesene Gruppen</th>
                                <th>Zuletzt geändert</th>
                            </tr>
                        </thead>
                        <tbody>
    "@)
    
            # Sortiert nach Name
            $sorted = $group.Group | Sort-Object Name
            foreach ($res in $sorted) {
                $includeTags = Format-AssignmentTags -Assignments ($res.Assignments | Where-Object { $_.AssignmentType -ne 'Exclude' })
                $excludeTags = Format-AssignmentTags -Assignments ($res.Assignments | Where-Object { $_.AssignmentType -eq 'Exclude' })
    
                # Kombinierte Tags: Include zuerst, dann Exclude
                $allTags = $includeTags
                $excludeOnly = $res.Assignments | Where-Object { $_.AssignmentType -eq 'Exclude' }
                if ($excludeOnly) {
                    $allTags += (Format-AssignmentTags -Assignments $excludeOnly)
                }
    
                $escapedName = [System.Web.HttpUtility]::HtmlEncode($res.Name)
                $escapedType = [System.Web.HttpUtility]::HtmlEncode($res.Type)
                $escapedSub  = [System.Web.HttpUtility]::HtmlEncode($res.SubCategory)
                $modified     = Format-DateTime -DateString $res.LastModifiedDateTime
    
                [void]$sb.Append(@"
                            <tr>
                                <td><strong>$escapedName</strong></td>
                                <td>$escapedType</td>
                                <td>$escapedSub</td>
                                <td>$allTags</td>
                                <td>$modified</td>
                            </tr>
    "@)
            }
    
            [void]$sb.Append(@"
                        </tbody>
                    </table>
                </div>
            </div>
    "@)
        }
    
        [void]$sb.Append('</div>')
        [void]$sb.Append((Get-HtmlFooter))
    
        # Datei schreiben
        $sb.ToString() | Out-File -FilePath $OutputFile -Encoding UTF8 -Force
        Write-Status "Ressourcen-Report erstellt: $OutputFile" -Type Success
    }
    
    function New-GroupReport {
        <#
        .SYNOPSIS
            Erstellt den HTML-Report aus Gruppen-Perspektive.
        .DESCRIPTION
            Invertiert das Mapping: Für jede Gruppe werden alle zugewiesenen
            Intune-Ressourcen aufgelistet, gruppiert nach Kategorie.
        #>
        param(
            [array]$Resources,
            [string]$OutputFile
        )
    
        Write-Status "Erstelle Gruppen-Report..."
    
        # Inverses Mapping aufbauen: GroupId/Name -> Liste von Ressourcen
        $groupMap = @{}   # Key: GroupId oder 'AllUsers'/'AllDevices'
        $groupNames = @{} # Key: GroupId -> Name
    
        foreach ($res in $Resources) {
            foreach ($a in $res.Assignments) {
                $key = switch ($a.AssignmentType) {
                    'AllUsers'   { '__AllUsers__' }
                    'AllDevices' { '__AllDevices__' }
                    default      { $a.GroupId }
                }
    
                if (-not $groupMap.ContainsKey($key)) {
                    $groupMap[$key] = [System.Collections.Generic.List[PSCustomObject]]::new()
                }
    
                $groupMap[$key].Add([PSCustomObject]@{
                    Category       = $res.Category
                    SubCategory    = $res.SubCategory
                    Name           = $res.Name
                    Type           = $res.Type
                    AssignmentType = $a.AssignmentType
                })
    
                # Name merken
                if (-not $groupNames.ContainsKey($key)) {
                    $groupNames[$key] = switch ($key) {
                        '__AllUsers__'   { 'Alle lizenzierten Benutzer' }
                        '__AllDevices__' { 'Alle Geräte' }
                        default          { $a.GroupName }
                    }
                }
            }
        }
    
        # Sortierung: Spezialgruppen zuerst, dann alphabetisch
        $sortedKeys = $groupMap.Keys | Sort-Object {
            switch ($_) {
                '__AllUsers__'   { '0_' }
                '__AllDevices__' { '1_' }
                default          { "2_$($groupNames[$_])" }
            }
        }
    
        $sb = [System.Text.StringBuilder]::new()
    
        # HTML Head
        [void]$sb.Append((Get-HtmlHead -Title "Intune Gruppen-Report"))
    
        # Header
        [void]$sb.Append(@"
        <div class="header">
            <h1>Intune Assignment Report &ndash; Gruppen-Sicht</h1>
            <div class="subtitle">Übersicht aller Gruppen und deren zugewiesene Intune-Ressourcen</div>
            <div class="meta">Erstellt am $(Get-Date -Format 'dd.MM.yyyy') um $(Get-Date -Format 'HH:mm') Uhr &bull; Tenant: $((Get-MgContext).TenantId)</div>
        </div>
        <div class="container">
    "@)
    
        # Summary
        $totalGroups = $groupMap.Count
        $maxAssignments = ($groupMap.Values | ForEach-Object { $_.Count } | Measure-Object -Maximum).Maximum
        $avgAssignments = [math]::Round(($groupMap.Values | ForEach-Object { $_.Count } | Measure-Object -Average).Average, 1)
    
        [void]$sb.Append(@"
            <div class="summary-grid">
                <div class="summary-card"><div class="label">Gruppen mit Zuweisungen</div><div class="value">$totalGroups</div></div>
                <div class="summary-card"><div class="label">Max. Zuweisungen pro Gruppe</div><div class="value">$maxAssignments</div></div>
                <div class="summary-card"><div class="label">Ø Zuweisungen pro Gruppe</div><div class="value">$avgAssignments</div></div>
            </div>
    "@)
    
        # TOC
        [void]$sb.Append('<div class="toc"><h3>Gruppen</h3>')
        foreach ($key in $sortedKeys) {
            $name = $groupNames[$key]
            $anchorId = ($key -replace '[^a-zA-Z0-9]', '').ToLower()
            $count = $groupMap[$key].Count
            [void]$sb.Append("<a href=`"#g-$anchorId`">$([System.Web.HttpUtility]::HtmlEncode($name)) ($count)</a>")
        }
        [void]$sb.Append('</div>')
    
        # Pro Gruppe eine Sektion
        foreach ($key in $sortedKeys) {
            $name = $groupNames[$key]
            $anchorId = ($key -replace '[^a-zA-Z0-9]', '').ToLower()
            $groupResources = $groupMap[$key]
    
            # Nach Kategorie gruppieren
            $byCategory = $groupResources | Group-Object -Property Category
    
            [void]$sb.Append(@"
            <div class="section group-section" id="g-$anchorId">
                <div class="group-header">
                    <h3>$([System.Web.HttpUtility]::HtmlEncode($name))</h3>
                    <span class="badge">$($groupResources.Count) Zuweisungen</span>
                </div>
                <div class="group-resources">
    "@)
    
            foreach ($catGroup in ($byCategory | Sort-Object Name)) {
                [void]$sb.Append("<div class=`"resource-category`"><div class=`"cat-label`">$([System.Web.HttpUtility]::HtmlEncode($catGroup.Name))</div>")
    
                foreach ($r in ($catGroup.Group | Sort-Object Name)) {
                    $escapedName = [System.Web.HttpUtility]::HtmlEncode($r.Name)
                    $escapedType = [System.Web.HttpUtility]::HtmlEncode($r.Type)
                    $assignLabel = ''
                    if ($r.AssignmentType -eq 'Exclude') { $assignLabel = '<span class="tag tag-exclude">Exclude</span>' }
    
                    [void]$sb.Append(@"
                    <div class="resource-item">
                        <span class="res-name">$escapedName $assignLabel</span>
                        <span class="res-type">$escapedType</span>
                    </div>
    "@)
                }
    
                [void]$sb.Append('</div>')
            }
    
            [void]$sb.Append('</div></div>')
        }
    
        [void]$sb.Append('</div>')
        [void]$sb.Append((Get-HtmlFooter))
    
        $sb.ToString() | Out-File -FilePath $OutputFile -Encoding UTF8 -Force
        Write-Status "Gruppen-Report erstellt: $OutputFile" -Type Success
    }
    
    # ============================================================================
    # Region: Interaktives Menü und Hauptlogik
    # ============================================================================
    
    function Show-Menu {
        <#
        .SYNOPSIS
            Zeigt ein interaktives Auswahlmenü an.
        #>
        Write-Host ""
        Write-Host "╔══════════════════════════════════════════════════╗" -ForegroundColor Cyan
        Write-Host "║        Intune Assignment Report - itelio         ║" -ForegroundColor Cyan
        Write-Host "╠══════════════════════════════════════════════════╣" -ForegroundColor Cyan
        Write-Host "║                                                  ║" -ForegroundColor Cyan
        Write-Host "║   [1] Ressourcen-Sicht                           ║" -ForegroundColor Cyan
        Write-Host "║       Alle Intune-Objekte mit Zuweisungen        ║" -ForegroundColor Cyan
        Write-Host "║                                                  ║" -ForegroundColor Cyan
        Write-Host "║   [2] Gruppen-Sicht                              ║" -ForegroundColor Cyan
        Write-Host "║       Alle Gruppen mit zugewiesenen Objekten     ║" -ForegroundColor Cyan
        Write-Host "║                                                  ║" -ForegroundColor Cyan
        Write-Host "║   [3] Beide Reports erstellen                    ║" -ForegroundColor Cyan
        Write-Host "║                                                  ║" -ForegroundColor Cyan
        Write-Host "║   [0] Beenden                                    ║" -ForegroundColor Cyan
        Write-Host "║                                                  ║" -ForegroundColor Cyan
        Write-Host "╚══════════════════════════════════════════════════╝" -ForegroundColor Cyan
        Write-Host ""
    
        do {
            $choice = Read-Host "Auswahl"
        } while ($choice -notin @('1', '2', '3', '0'))
    
        $result = switch ($choice) {
            '1' { 'Ressourcen' }
            '2' { 'Gruppen' }
            '3' { 'Beide' }
            '0' { 'Exit' }
        }
        return $result
    }
    
    # ============================================================================
    # Region: Hauptprogramm
    # ============================================================================
    
    # System.Web laden für HTML-Encoding
    Add-Type -AssemblyName System.Web
    
    # Report-Modus bestimmen (Parameter oder interaktiv)
    if (-not $ReportMode) {
        $menuChoice = Show-Menu
        if ($menuChoice -eq 'Exit') {
            # Graph-Verbindung trennen, falls eine besteht
            if (Get-MgContext -ErrorAction SilentlyContinue) {
                Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
                Write-Status "Graph-Verbindung getrennt."
            }
            Write-Status "Beendet."
            return
        }
        $ReportMode = $menuChoice
    }
    
    # Ausgabeverzeichnis bestimmen (nur interaktiv, wenn nicht per Parameter übergeben)
    if (-not $OutputPath) {
        $defaultPath = Join-Path $env:USERPROFILE "Intune-Reports"
        Write-Host ""
        Write-Host "Ausgabeverzeichnis fuer die HTML-Reports" -ForegroundColor Cyan
        Write-Host "Standard: $defaultPath" -ForegroundColor Gray
        $userInput = Read-Host "Verzeichnis eingeben (Enter fuer Standard)"
    
        if ([string]::IsNullOrWhiteSpace($userInput)) {
            $OutputPath = $defaultPath
        } else {
            $OutputPath = $userInput.Trim()
        }
    }
    
    # Verzeichnis prüfen und ggf. erstellen
    if (-not (Test-Path $OutputPath)) {
        Write-Host ""
        $confirm = Read-Host "Verzeichnis '$OutputPath' existiert nicht. Erstellen? (J/N)"
        if ($confirm -in @('J', 'j', 'Y', 'y')) {
            try {
                New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
                Write-Status "Verzeichnis erstellt: $OutputPath" -Type Success
            }
            catch {
                Write-Status "Verzeichnis konnte nicht erstellt werden: $($_.Exception.Message)" -Type Error
                return
            }
        } else {
            Write-Status "Abgebrochen. Kein Ausgabeverzeichnis verfuegbar." -Type Error
            return
        }
    }
    
    # Graph-Verbindung prüfen/herstellen
    $context = Get-MgContext
    if (-not $context) {
        Write-Status "Keine aktive Graph-Verbindung. Starte Authentifizierung..."
        try {
            Connect-MgGraph -Scopes @(
                "DeviceManagementManagedDevices.Read.All",
                "DeviceManagementConfiguration.Read.All",
                "DeviceManagementApps.Read.All",
                "DeviceManagementServiceConfig.Read.All",
                "DeviceManagementScripts.Read.All",
                "Group.Read.All"
            ) -ErrorAction Stop
            Write-Status "Graph-Verbindung hergestellt." -Type Success
        }
        catch {
            Write-Status "Authentifizierung fehlgeschlagen: $($_.Exception.Message)" -Type Error
            Write-Status "Bitte sicherstellen, dass das Modul 'Microsoft.Graph.Authentication' installiert ist." -Type Error
            return
        }
    }
    else {
        Write-Status "Graph-Verbindung aktiv - Tenant: $($context.TenantId)"
    }
    
    # Zeitstempel für Dateinamen
    $timestamp = Get-Date -Format 'yyyyMMdd-HHmm'
    
    # Gruppen-Lookup laden
    $groupLookup = Get-AllGroupsLookup
    
    # Intune-Ressourcen sammeln
    Write-Status "Starte Datensammlung aller Intune-Ressourcen..."
    $startTime = Get-Date
    
    $resources = Get-IntuneResources -GroupLookup $groupLookup
    
    $duration = (Get-Date) - $startTime
    Write-Status "Datensammlung abgeschlossen: $($resources.Count) Ressourcen in $($duration.ToString('mm\:ss')) min." -Type Success
    
    # Reports erstellen
    if ($ReportMode -in @('Ressourcen', 'Beide')) {
        $resourceFile = Join-Path $OutputPath "IntuneReport_Ressourcen_$timestamp.html"
        New-ResourceReport -Resources $resources -OutputFile $resourceFile
    }
    
    if ($ReportMode -in @('Gruppen', 'Beide')) {
        $groupFile = Join-Path $OutputPath "IntuneReport_Gruppen_$timestamp.html"
        New-GroupReport -Resources $resources -OutputFile $groupFile
    }
    
    # Abschluss
    Write-Host ""
    Write-Status "═══════════════════════════════════════════════" -Type Success
    Write-Status "Reports gespeichert in: $OutputPath" -Type Success
    Write-Status "═══════════════════════════════════════════════" -Type Success
    
    # Reports im Browser öffnen
    if ($ReportMode -in @('Ressourcen', 'Beide')) {
        Write-Status "Öffne Ressourcen-Report..."
        Start-Process $resourceFile
    }
    if ($ReportMode -in @('Gruppen', 'Beide')) {
        Write-Status "Öffne Gruppen-Report..."
        Start-Process $groupFile
    }
    
    # Graph-Verbindung trennen
    try {
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        Write-Status "Graph-Verbindung getrennt."
    }
    catch {
        # Kein Fehler wenn keine Verbindung bestand
    }