Public/Invoke-IntuneHydration.ps1
|
#Requires -Version 7.0 function Invoke-IntuneHydration { <# .SYNOPSIS Main orchestrator function for Intune tenant hydration .DESCRIPTION Executes the complete hydration workflow including authentication, pre-flight checks, and import of all baseline configurations. Two mutually exclusive invocation modes: 1. Settings File Mode: Use -SettingsPath to load all configuration from a JSON file 2. Parameter Mode: Use -Interactive or -ClientId/-ClientSecret with other parameters These modes cannot be mixed - choose one or the other. .PARAMETER SettingsPath Path to the settings JSON file. Use this for settings file-based invocation. Cannot be combined with -Interactive, -ClientId, or -ClientSecret. .PARAMETER TenantId Azure AD tenant ID (GUID). Required for parameter-based invocation. .PARAMETER TenantName Tenant name for display purposes (e.g., contoso.onmicrosoft.com) .PARAMETER Interactive Use interactive authentication (browser-based login). Cannot be combined with -SettingsPath. .PARAMETER ClientId Application (client) ID for service principal authentication. Cannot be combined with -SettingsPath. .PARAMETER ClientSecret Client secret for service principal authentication (SecureString). Cannot be combined with -SettingsPath. .PARAMETER Environment Azure cloud environment. Valid values: Global, USGov, USGovDoD, Germany, China .PARAMETER Create Enable creation of configurations .PARAMETER Delete Enable deletion of kit-created configurations .PARAMETER Force Skip confirmation prompt when running in delete mode (available for both settings-file and parameter modes) .PARAMETER VerboseOutput Enable verbose logging output .PARAMETER OpenIntuneBaseline Process OpenIntuneBaseline policies .PARAMETER ComplianceTemplates Process compliance policy templates .PARAMETER AppProtection Process app protection policies .PARAMETER NotificationTemplates Process notification templates .PARAMETER EnrollmentProfiles Process enrollment profiles (Autopilot, ESP) .PARAMETER DynamicGroups Process dynamic groups .PARAMETER StaticGroups Process static (assigned) groups .PARAMETER DeviceFilters Process device filters .PARAMETER ConditionalAccess Process Conditional Access starter pack policies .PARAMETER MobileApps Process mobile app templates .PARAMETER All Enable all targets .PARAMETER BaselineRepoUrl GitHub repository URL for OpenIntuneBaseline .PARAMETER BaselineBranch Git branch to use for OpenIntuneBaseline .PARAMETER BaselineDownloadPath Local path for OpenIntuneBaseline download .PARAMETER ReportOutputPath Output directory for reports .PARAMETER ReportFormats Report formats to generate (markdown, json) .EXAMPLE Invoke-IntuneHydration -SettingsPath ./settings.json Run using settings from a JSON file. .EXAMPLE Invoke-IntuneHydration -SettingsPath ./settings.json -WhatIf Dry-run using settings file. .EXAMPLE Invoke-IntuneHydration -TenantId "00000000-0000-0000-0000-000000000000" -Interactive -Create -All Run with all imports enabled using interactive authentication. .EXAMPLE Invoke-IntuneHydration -TenantId "00000000-0000-0000-0000-000000000000" -ClientId "client-id" -ClientSecret $secret -Create -ComplianceTemplates -DynamicGroups Run with service principal authentication and specific imports enabled. .EXAMPLE Invoke-IntuneHydration -TenantId "00000000-0000-0000-0000-000000000000" -Interactive -Delete -All -WhatIf Dry-run delete mode with interactive authentication. #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'SettingsFile')] param( # Settings file parameter - exclusive mode [Parameter(ParameterSetName = 'SettingsFile', Mandatory = $true, Position = 0)] [ValidateScript({ Test-Path $_ })] [string]$SettingsPath, # Tenant parameters - required for parameter-based modes [Parameter(ParameterSetName = 'Interactive', Mandatory = $true)] [Parameter(ParameterSetName = 'ServicePrincipal', Mandatory = $true)] [ValidatePattern('^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')] [string]$TenantId, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [string]$TenantName, # Authentication parameters - Interactive mode [Parameter(ParameterSetName = 'Interactive', Mandatory = $true)] [switch]$Interactive, # Authentication parameters - Service Principal mode [Parameter(ParameterSetName = 'ServicePrincipal', Mandatory = $true)] [string]$ClientId, [Parameter(ParameterSetName = 'ServicePrincipal', Mandatory = $true)] [SecureString]$ClientSecret, # Environment - available for parameter-based modes only [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [ValidateSet('Global', 'USGov', 'USGovDoD', 'Germany', 'China')] [string]$Environment = 'Global', # Options parameters - available for parameter-based modes only [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$Create, [Parameter(ParameterSetName = 'SettingsFile')] [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$Delete, [Parameter(ParameterSetName = 'SettingsFile')] [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$Force, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$VerboseOutput, # Target enable switches - available for parameter-based modes only [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$OpenIntuneBaseline, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$ComplianceTemplates, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$AppProtection, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$NotificationTemplates, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$EnrollmentProfiles, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$DynamicGroups, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$StaticGroups, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$DeviceFilters, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$ConditionalAccess, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$MobileApps, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [switch]$All, # OpenIntuneBaseline parameters - available for parameter-based modes only [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [string]$BaselineRepoUrl = "https://github.com/SkipToTheEndpoint/OpenIntuneBaseline", [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [string]$BaselineBranch = 'main', [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [string]$BaselineDownloadPath, # Reporting parameters - available for parameter-based modes only [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [string]$ReportOutputPath, [Parameter(ParameterSetName = 'Interactive')] [Parameter(ParameterSetName = 'ServicePrincipal')] [ValidateSet('markdown', 'json')] [string[]]$ReportFormats ) $ErrorActionPreference = 'Stop' $InformationPreference = 'Continue' # Resolve module root - use $script:ModuleRoot if set by psm1, otherwise use PSScriptRoot parent $moduleRoot = if ($script:ModuleRoot) { $script:ModuleRoot } else { Split-Path -Path $PSScriptRoot -Parent } #region Main Execution try { # Initialize settings based on parameter set $settings = $null if ($PSCmdlet.ParameterSetName -eq 'SettingsFile') { # Settings file mode - load everything from the file $settings = Import-HydrationSettings -Path $SettingsPath Write-Host "Loaded settings from: $SettingsPath" -InformationAction Continue if (-not $settings.options) { $settings['options'] = @{} } # Set force option from parameter, preserving any existing force setting from settings file $settings.options.force = $Force.IsPresent -or ($settings.options.ContainsKey('force') -and $settings.options.force) } else { # Parameter-based mode - build settings from parameters Write-Host "Using parameter-based configuration" -InformationAction Continue # Determine which targets are enabled $importsEnabled = @{ dynamicGroups = $All.IsPresent -or $DynamicGroups.IsPresent staticGroups = $All.IsPresent -or $StaticGroups.IsPresent deviceFilters = $All.IsPresent -or $DeviceFilters.IsPresent conditionalAccess = $All.IsPresent -or $ConditionalAccess.IsPresent complianceTemplates = $All.IsPresent -or $ComplianceTemplates.IsPresent openIntuneBaseline = $All.IsPresent -or $OpenIntuneBaseline.IsPresent enrollmentProfiles = $All.IsPresent -or $EnrollmentProfiles.IsPresent appProtection = $All.IsPresent -or $AppProtection.IsPresent notificationTemplates = $All.IsPresent -or $NotificationTemplates.IsPresent mobileApps = $All.IsPresent -or $MobileApps.IsPresent } # Validate that at least one target is enabled if (-not ($importsEnabled.Values -contains $true)) { throw "At least one target must be enabled. Use -All or specify a target switch (e.g., -DynamicGroups, -DeviceFilters, etc.)." } # Build settings object from parameters $settings = @{ tenant = @{ tenantId = $TenantId tenantName = $TenantName } authentication = @{ mode = if ($Interactive) { 'interactive' } else { 'clientSecret' } clientId = $ClientId clientSecret = if ($ClientSecret) { [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ClientSecret)) } else { $null } environment = $Environment } options = @{ create = $Create.IsPresent delete = $Delete.IsPresent force = $Force.IsPresent dryRun = [bool]$WhatIfPreference verbose = $VerboseOutput.IsPresent } imports = $importsEnabled openIntuneBaseline = @{ repoUrl = $BaselineRepoUrl branch = $BaselineBranch downloadPath = if ($BaselineDownloadPath) { $BaselineDownloadPath } else { './OpenIntuneBaseline' } } reporting = @{ outputPath = if ($ReportOutputPath) { $ReportOutputPath } else { 'Reports' } formats = if ($ReportFormats) { $ReportFormats } else { @('markdown') } } } } # Display current settings Write-Host "Target Tenant: $($settings.tenant.tenantId)" -InformationAction Continue if ($settings.tenant.tenantName) { Write-Host "Tenant Name: $($settings.tenant.tenantName)" -InformationAction Continue } Write-Host "Authentication Mode: $($settings.authentication.mode)" -InformationAction Continue Write-Host "Options:" -InformationAction Continue Write-Host ($settings.options | Out-String) -InformationAction Continue Write-Host "Imports Enabled:" -InformationAction Continue Write-Host ($settings.imports | Out-String) -InformationAction Continue # Apply options from settings $createEnabled = $settings.options.create -eq $true $deleteEnabled = $settings.options.delete -eq $true $forceDelete = $settings.options.force -eq $true $RemoveExisting = $deleteEnabled # Validate options - create and delete are mutually exclusive if ($createEnabled -and $deleteEnabled) { throw "Only one of 'create' or 'delete' options can be true. Current settings: create=$createEnabled, delete=$deleteEnabled" } if (-not $createEnabled -and -not $deleteEnabled) { throw "At least one of 'create' or 'delete' options must be true. Current settings: create=$createEnabled, delete=$deleteEnabled" } if ($deleteEnabled -and -not $forceDelete -and -not $WhatIfPreference) { if (-not $PSCmdlet.ShouldContinue("Proceed with delete operations?", "Delete mode will remove Intune configurations created by the hydration kit.")) { Write-Warning "Delete operation cancelled by user confirmation." return } } # dryRun from settings enables WhatIf if not already set via command line if ($settings.options.dryRun -eq $true -and -not $WhatIfPreference) { $script:WhatIfPreference = $true } # verbose from settings enables verbose output if ($settings.options.verbose -eq $true) { $script:VerbosePreference = 'Continue' } # Initialize logging (after applying verbose setting) $logsPath = Join-Path -Path $moduleRoot -ChildPath 'Logs' Initialize-HydrationLogging -LogPath $logsPath -EnableVerbose:($VerbosePreference -eq 'Continue') Write-HydrationLog -Message "=== Intune Hydration Kit Started ===" -Level Info Write-HydrationLog -Message "Loaded settings for tenant: $(Get-ObfuscatedTenantId -TenantId $settings.tenant.tenantId)" -Level Info if ($WhatIfPreference) { Write-HydrationLog -Message "Running in DRY-RUN mode - no changes will be made" -Level Warning } if ($RemoveExisting) { if (-not $createEnabled) { Write-HydrationLog -Message "DELETE-ONLY mode - configurations will be deleted without recreation" -Level Warning } else { Write-HydrationLog -Message "Remove existing enabled - matching configurations will be deleted before import" -Level Warning } } # Initialize results tracking $allResults = @() # Step 1: Authenticate Write-HydrationLog -Message "Step 1: Authenticating to Microsoft Graph" -Level Info $authParams = @{ TenantId = $settings.tenant.tenantId } # Add environment if specified if ($settings.authentication.environment) { $authParams['Environment'] = $settings.authentication.environment } if ($settings.authentication.mode -eq 'clientSecret') { $authParams['ClientId'] = $settings.authentication.clientId $authParams['ClientSecret'] = $settings.authentication.clientSecret | ConvertTo-SecureString -AsPlainText -Force } else { $authParams['Interactive'] = $true } # Always connect to Graph API (needed for dry-run to check existing policies) Connect-IntuneHydration @authParams # Step 2: Pre-flight checks Write-HydrationLog -Message "Step 2: Running pre-flight checks" -Level Info # Always run pre-flight checks (read-only operations) Test-IntunePrerequisites | Out-Null # Step 3: Dynamic Groups if ($settings.imports.dynamicGroups) { $stepAction = if ($RemoveExisting) { "Deleting" } else { "Creating" } Write-HydrationLog -Message "Step 3: $stepAction Dynamic Groups" -Level Info # Delete existing dynamic groups if RemoveExisting is set # SAFETY: Only delete groups that have "Imported by Intune-Hydration-Kit" in description if ($RemoveExisting) { try { # Get all dynamic groups with descriptions $listUri = "beta/groups?`$filter=groupTypes/any(c:c eq 'DynamicMembership')&`$select=id,displayName,description" do { $existingGroups = Invoke-MgGraphRequest -Method GET -Uri $listUri -ErrorAction Stop foreach ($group in $existingGroups.value) { # Safety check: Only delete if created by this kit (has hydration marker in description) if (-not (Test-HydrationKitObject -Description $group.description -ObjectName $group.displayName)) { Write-Verbose "Skipping '$($group.displayName)' - not created by Intune-Hydration-Kit" continue } if ($PSCmdlet.ShouldProcess($group.displayName, "Delete dynamic group")) { try { Invoke-MgGraphRequest -Method DELETE -Uri "beta/groups/$($group.id)" -ErrorAction Stop Write-HydrationLog -Message " Deleted: $($group.displayName)" -Level Info $allResults += New-HydrationResult -Type 'DynamicGroup' -Name $group.displayName -Action 'Deleted' -Status 'Success' } catch { Write-HydrationLog -Message "Failed to delete group '$($group.displayName)': $_" -Level Warning $allResults += New-HydrationResult -Type 'DynamicGroup' -Name $group.displayName -Action 'Failed' -Status $_.Exception.Message } } else { $allResults += New-HydrationResult -Type 'DynamicGroup' -Name $group.displayName -Action 'WouldDelete' -Status 'DryRun' } } $listUri = $existingGroups.'@odata.nextLink' } while ($listUri) } catch { Write-HydrationLog -Message "Failed to list dynamic groups: $_" -Level Warning } } else { # Normal create mode $groupsTemplatePath = Join-Path -Path $moduleRoot -ChildPath 'Templates/DynamicGroups' if (Test-Path -Path $groupsTemplatePath) { $groupTemplates = Get-ChildItem -Path $groupsTemplatePath -Filter "*.json" -File # Collect all groups from templates $allGroupDefs = @() foreach ($templateFile in $groupTemplates) { $templateContent = Get-Content -Path $templateFile.FullName -Raw | ConvertFrom-Json # Handle templates with multiple groups $groups = if ($templateContent.groups) { $templateContent.groups } else { @($templateContent) } $allGroupDefs += $groups } foreach ($groupDef in $allGroupDefs) { if ($PSCmdlet.ShouldProcess($groupDef.displayName, "Create dynamic group")) { $groupResult = New-IntuneDynamicGroup -DisplayName $groupDef.displayName -Description $groupDef.description -MembershipRule $groupDef.membershipRule $allResults += New-HydrationResult -Type 'DynamicGroup' -Name $groupDef.displayName -Action $groupResult.Action -Id $groupResult.Id -Details $groupResult.Reason Write-HydrationLog -Message " $($groupResult.Action): $($groupDef.displayName)" -Level Info } } } else { Write-HydrationLog -Message "Dynamic Groups template directory not found" -Level Warning } } } # Step 3b: Static Groups if ($settings.imports.staticGroups) { $stepAction = if ($RemoveExisting) { "Deleting" } else { "Creating" } Write-HydrationLog -Message "Step 3b: $stepAction Static Groups" -Level Info # Delete existing static groups if RemoveExisting is set # SAFETY: Only delete groups that have "Imported by Intune-Hydration-Kit" in description if ($RemoveExisting) { try { # Get all security groups (non-dynamic) with hydration kit marker in description # Note: Using ConsistencyLevel header and $count for advanced query with NOT operator $listUri = "beta/groups?`$filter=securityEnabled eq true and NOT groupTypes/any(c:c eq 'DynamicMembership')&`$select=id,displayName,description&`$count=true" $headers = @{ 'ConsistencyLevel' = 'eventual' } do { $existingGroups = Invoke-MgGraphRequest -Method GET -Uri $listUri -Headers $headers -ErrorAction Stop foreach ($group in $existingGroups.value) { # Safety check: Only delete if created by this kit (has hydration marker in description) if (-not (Test-HydrationKitObject -Description $group.description -ObjectName $group.displayName)) { Write-Verbose "Skipping '$($group.displayName)' - not created by Intune-Hydration-Kit" continue } if ($PSCmdlet.ShouldProcess($group.displayName, "Delete static group")) { try { Invoke-MgGraphRequest -Method DELETE -Uri "beta/groups/$($group.id)" -ErrorAction Stop Write-HydrationLog -Message " Deleted: $($group.displayName)" -Level Info $allResults += New-HydrationResult -Type 'StaticGroup' -Name $group.displayName -Action 'Deleted' -Status 'Success' } catch { Write-HydrationLog -Message "Failed to delete group '$($group.displayName)': $_" -Level Warning $allResults += New-HydrationResult -Type 'StaticGroup' -Name $group.displayName -Action 'Failed' -Status $_.Exception.Message } } else { $allResults += New-HydrationResult -Type 'StaticGroup' -Name $group.displayName -Action 'WouldDelete' -Status 'DryRun' } } $listUri = $existingGroups.'@odata.nextLink' } while ($listUri) } catch { Write-HydrationLog -Message "Failed to list static groups: $_" -Level Warning } } else { # Normal create mode $staticGroupsTemplatePath = Join-Path -Path $moduleRoot -ChildPath 'Templates/StaticGroups' if (Test-Path -Path $staticGroupsTemplatePath) { $groupTemplates = Get-ChildItem -Path $staticGroupsTemplatePath -Filter "*.json" -File # Collect all groups from templates $allGroupDefs = @() foreach ($templateFile in $groupTemplates) { $templateContent = Get-Content -Path $templateFile.FullName -Raw | ConvertFrom-Json # Handle templates with multiple groups $groups = if ($templateContent.groups) { $templateContent.groups } else { @($templateContent) } $allGroupDefs += $groups } foreach ($groupDef in $allGroupDefs) { if ($PSCmdlet.ShouldProcess($groupDef.displayName, "Create static group")) { $groupResult = New-IntuneStaticGroup -DisplayName $groupDef.displayName -Description $groupDef.description $allResults += New-HydrationResult -Type 'StaticGroup' -Name $groupDef.displayName -Action $groupResult.Action -Id $groupResult.Id -Details $groupResult.Reason Write-HydrationLog -Message " $($groupResult.Action): $($groupDef.displayName)" -Level Info } } } else { Write-HydrationLog -Message "Static Groups template directory not found" -Level Warning } } } # Step 4: Device Filters if ($settings.imports.deviceFilters) { $stepAction = if ($RemoveExisting) { "Deleting" } else { "Creating" } Write-HydrationLog -Message "Step 4: $stepAction Device Filters" -Level Info $filterResults = Import-IntuneDeviceFilter -RemoveExisting:$RemoveExisting -WhatIf:$WhatIfPreference $allResults += $filterResults } # Step 5: OpenIntuneBaseline if ($settings.imports.openIntuneBaseline) { $stepAction = if ($RemoveExisting) { "Deleting" } else { "Importing" } Write-HydrationLog -Message "Step 5: $stepAction OpenIntuneBaseline policies" -Level Info $baselineParams = @{} if ($settings.openIntuneBaseline.downloadPath) { $baselineParams['BaselinePath'] = $settings.openIntuneBaseline.downloadPath } # Import function handles ShouldProcess internally for each policy $baselineParams['RemoveExisting'] = $RemoveExisting $baselineParams['WhatIf'] = $WhatIfPreference $baselineResults = Import-IntuneBaseline @baselineParams $allResults += $baselineResults } # Step 6: Compliance Templates if ($settings.imports.complianceTemplates) { $stepAction = if ($RemoveExisting) { "Deleting" } else { "Importing" } Write-HydrationLog -Message "Step 6: $stepAction Compliance templates" -Level Info $complianceResults = Import-IntuneCompliancePolicy -RemoveExisting:$RemoveExisting -WhatIf:$WhatIfPreference $allResults += $complianceResults } # Step 7: Notification Templates if ($settings.imports.notificationTemplates) { $stepAction = if ($RemoveExisting) { "Deleting" } else { "Importing" } Write-HydrationLog -Message "Step 7: $stepAction Notification Templates" -Level Info $notificationResults = Import-IntuneNotificationTemplate -RemoveExisting:$RemoveExisting -WhatIf:$WhatIfPreference $allResults += $notificationResults } # Step 8: App Protection Policies (MAM) if ($settings.imports.appProtection) { $stepAction = if ($RemoveExisting) { "Deleting" } else { "Importing" } Write-HydrationLog -Message "Step 8: $stepAction App Protection policies" -Level Info $mamResults = Import-IntuneAppProtectionPolicy -RemoveExisting:$RemoveExisting -WhatIf:$WhatIfPreference $allResults += $mamResults } # Step 9: Enrollment Profiles if ($settings.imports.enrollmentProfiles) { $stepAction = if ($RemoveExisting) { "Deleting" } else { "Importing" } Write-HydrationLog -Message "Step 9: $stepAction Enrollment Profiles" -Level Info $enrollmentResults = Import-IntuneEnrollmentProfile -RemoveExisting:$RemoveExisting -WhatIf:$WhatIfPreference $allResults += $enrollmentResults } # Step 10: Conditional Access Starter Pack if ($settings.imports.conditionalAccess) { $stepAction = if ($RemoveExisting) { "Deleting" } else { "Importing" } Write-HydrationLog -Message "Step 10: $stepAction Conditional Access Starter Pack" -Level Info $caResults = Import-IntuneConditionalAccessPolicy -RemoveExisting:$RemoveExisting -WhatIf:$WhatIfPreference $allResults += $caResults } # Step 11: Mobile Apps if ($settings.imports.mobileApps) { $stepAction = if ($RemoveExisting) { "Deleting" } else { "Importing" } Write-HydrationLog -Message "Step 11: $stepAction Mobile Apps" -Level Info $mobileAppResults = Import-IntuneMobileApp -RemoveExisting:$RemoveExisting -WhatIf:$WhatIfPreference $allResults += $mobileAppResults } # Step 12: Generate Summary Report Write-HydrationLog -Message "Step 12: Generating Summary Report" -Level Info $reportsPath = Join-Path -Path $moduleRoot -ChildPath $settings.reporting.outputPath if (-not (Test-Path -Path $reportsPath)) { New-Item -Path $reportsPath -ItemType Directory -Force | Out-Null } $summary = Get-ResultSummary -Results $allResults # Generate markdown report $reportPath = Join-Path -Path $reportsPath -ChildPath "Hydration-Summary.md" $jsonReportPath = $null $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $reportContent = @" # Intune Hydration Summary **Generated:** $timestamp **Tenant:** $($settings.tenant.tenantId) **Environment:** $($settings.authentication.environment) **Mode:** $(if ($WhatIfPreference) { 'Dry-Run' } else { 'Live' }) ## Summary | Metric | Count | |--------|-------| | Total Operations | $($allResults.Count) | | Created | $($summary.Created) | | Updated | $($summary.Updated) | | Skipped | $($summary.Skipped) | | Would Create | $($summary.WouldCreate) | | Would Update | $($summary.WouldUpdate) | | Failed | $($summary.Failed) | ## Details by Type "@ # Group results by type $byType = $allResults | Group-Object -Property Type foreach ($typeGroup in $byType) { $typeResults = $typeGroup.Group $created = ($typeResults | Where-Object { $_.Action -eq 'Created' }).Count $updated = ($typeResults | Where-Object { $_.Action -eq 'Updated' }).Count $skipped = ($typeResults | Where-Object { $_.Action -eq 'Skipped' }).Count $wouldCreate = ($typeResults | Where-Object { $_.Action -eq 'WouldCreate' }).Count $failed = ($typeResults | Where-Object { $_.Action -eq 'Failed' }).Count $wouldUpdate = ($typeResults | Where-Object { $_.Action -eq 'WouldUpdate' }).Count $reportContent += @" ### $($typeGroup.Name) - Created: $created - Updated: $updated - Skipped: $skipped - Would Create: $wouldCreate - Would Update: $wouldUpdate - Failed: $failed "@ } if ($allResults.Count -gt 0) { $reportContent += @" ## All Operations | Timestamp | Type | Name | Action | ID | Details | |-----------|------|------|--------|-----|---------| "@ foreach ($result in $allResults) { $reportContent += "| $($result.Timestamp) | $($result.Type) | $($result.Name) | $($result.Action) | $($result.Id) | $($result.Status) |`n" } } $reportContent += @" ## Important Notes - **Conditional Access policies** were created in **DISABLED** state. Review and enable as needed. - **OpenIntuneBaseline policies** were imported using IntuneManagement module. - Review all configurations before enabling in production. "@ $reportContent | Out-File -FilePath $reportPath -Encoding UTF8 Write-HydrationLog -Message "Summary report written to: $reportPath" -Level Info # Also write JSON if requested if ('json' -in $settings.reporting.formats) { $jsonReportPath = Join-Path -Path $reportsPath -ChildPath "Hydration-Summary.json" @{ Timestamp = $timestamp Tenant = $settings.tenant.tenantId Environment = $settings.authentication.environment Mode = if ($WhatIfPreference) { 'DryRun' } else { 'Live' } Summary = $summary Results = $allResults } | ConvertTo-Json -Depth 10 | Out-File -FilePath $jsonReportPath -Encoding UTF8 Write-HydrationLog -Message "JSON report written to: $jsonReportPath" -Level Info } Write-HydrationLog -Message "=== Intune Hydration Kit Completed ===" -Level Info # Friendly console summary Write-Host "" -InformationAction Continue Write-Host "---------------- Summary ----------------" -InformationAction Continue if ($WhatIfPreference) { Write-Host ("Would Create: {0} | Would Update: {1} | Would Delete: {2} | Skipped: {3} | Failed: {4}" -f $summary.WouldCreate, $summary.WouldUpdate, $summary.WouldDelete, $summary.Skipped, $summary.Failed) -InformationAction Continue } else { Write-Host ("Created: {0} | Updated: {1} | Deleted: {2} | Skipped: {3} | Failed: {4}" -f $summary.Created, $summary.Updated, $summary.Deleted, $summary.Skipped, $summary.Failed) -InformationAction Continue } Write-Host "Reports: $reportPath" -InformationAction Continue if ($jsonReportPath) { Write-Host "JSON: $jsonReportPath" -InformationAction Continue } Write-Host "----------------------------------------" -InformationAction Continue # Return summary object instead of exiting (functions shouldn't call exit) if ($summary.Failed -gt 0) { Write-HydrationLog -Message "Completed with $($summary.Failed) failures" -Level Warning return @{ Success = $false Summary = $summary Results = $allResults ReportPath = $reportPath JsonReportPath = $jsonReportPath } } else { if ($WhatIfPreference) { Write-HydrationLog -Message "Dry-run completed: $($summary.WouldCreate) would create, $($summary.WouldUpdate) would update, $($summary.WouldDelete) would delete, $($summary.Skipped) skipped" -Level Info } else { Write-HydrationLog -Message "Completed successfully: $($summary.Created) created, $($summary.Updated) updated, $($summary.Deleted) deleted, $($summary.Skipped) skipped" -Level Info } return @{ Success = $true Summary = $summary Results = $allResults ReportPath = $reportPath JsonReportPath = $jsonReportPath } } } catch { Write-HydrationLog -Message "Fatal error: $_" -Level Error throw } #endregion } |