# ========================================== # Intune / Graph Automation Script - Idempotent # ========================================== # Ensure Microsoft Graph modules are installed foreach ($module in @("Microsoft.Graph", "Microsoft.Graph.Beta")) { if (-not (Get-Module -ListAvailable -Name $module)) { Install-Module -Name $module -Scope CurrentUser -Force } } # Connect to Microsoft Graph Connect-MgGraph -Scopes "Device.ReadWrite.All","DeviceManagementConfiguration.ReadWrite.All","Organization.Read.All","Group.ReadWrite.All","Directory.ReadWrite.All" -NoWelcome # Get Tenant ID $tenant = Get-MgOrganization $tenantId = $tenant.Id # ------------------------- # Helper Functions # ------------------------- # ------------------------- # Helper Functions # ------------------------- function Get-OrCreateGroup { param( [string]$DisplayName, [string]$MailNickname, [string]$DynamicRule = $null ) if (-not $DisplayName) { throw "DisplayName cannot be empty" } # ---------------------------- # Fetch all groups (Graph SDK handles paging) # ---------------------------- $allGroups = Get-MgGroup -All $existingGroup = $allGroups | Where-Object { $_.displayName -eq $DisplayName } | Select-Object -First 1 if ($existingGroup) { Write-Host "⚠️ Group '$DisplayName' already exists" $null = return $existingGroup.Id } # ---------------------------- # Create new group # ---------------------------- $groupBody = @{ displayName = $DisplayName mailEnabled = $false mailNickname = $MailNickname securityEnabled = $true } if ($DynamicRule) { $groupBody.groupTypes = @("DynamicMembership") $groupBody.membershipRule = $DynamicRule $groupBody.membershipRuleProcessingState = "On" } $response = New-MgGroup -BodyParameter $groupBody Write-Host "✅ Created group '$DisplayName'" $null = return $response.Id } function Import-ConfigurationPolicy { param( [string]$PolicyPath, [string]$GroupId ) # Load the JSON policy $JsonData = Get-Content -Path $PolicyPath -Raw $JsonDataUpdated = $JsonData -replace '\$tenantId', $tenantId $PolicyObject = $JsonDataUpdated | ConvertFrom-Json if (-not $PolicyObject.name) { throw "Policy has no 'name': $PolicyPath" } # ---------------------------- # Fetch all existing configuration policies # ---------------------------- $allPolicies = @() $uri = "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies" do { $response = Invoke-MgGraphRequest -Method GET -Uri $uri $responseObj = if ($response -is [string]) { $response | ConvertFrom-Json } else { $response } if ($responseObj.value) { $allPolicies += $responseObj.value } $uri = $responseObj.'@odata.nextLink' } while ($uri) # ---------------------------- # Check if a policy with the same name exists # ---------------------------- $existing = $allPolicies | Where-Object { $_.name -eq $PolicyObject.name } | Select-Object -First 1 if ($existing) { Write-Host "⚠️ Policy '$($PolicyObject.name)' already exists" #Assign-PolicyToGroup -PolicyType "configurationPolicies" -PolicyId $existing.id -GroupId $GroupId $null = return $existing.id # <-- exit immediately, no creation } # ---------------------------- # Create the policy if it doesn't exist # ---------------------------- $response = Invoke-MgGraphRequest -Method POST ` -Uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies" ` -Body ($PolicyObject | ConvertTo-Json -Depth 100) ` -ContentType "application/json" Write-Host "✅ Policy '$($PolicyObject.name)' imported" Assign-PolicyToGroup -PolicyType "configurationPolicies" -PolicyId $response.id -GroupId $GroupId $null = return $response.id } function Assign-PolicyToGroup { param( [string]$PolicyType, [string]$PolicyId, [string]$GroupId ) if (-not $GroupId) { return } $uri = "https://graph.microsoft.com/beta/deviceManagement/$PolicyType/$PolicyId/assign" $body = @{ assignments = @( @{ target = @{ "@odata.type" = "#microsoft.graph.groupAssignmentTarget" groupId = $GroupId } } ) } Invoke-MgGraphRequest -Method POST -Uri $uri -Body ($body | ConvertTo-Json -Depth 100) Write-Host "✅ Group assigned to $PolicyType policy ID $PolicyId" } function Import-CompliancePolicy { param( [string]$PolicyPath, [string]$GroupId ) $JsonData = Get-Content -Path $PolicyPath -Raw $JsonDataUpdated = $JsonData -replace '\$tenantId', $tenantId $PolicyObject = $JsonDataUpdated | ConvertFrom-Json if (-not $PolicyObject.displayName) { throw "Policy has no 'displayName': $PolicyPath" } # ---------------------------- # Fetch all existing compliance policies via REST (paged) # ---------------------------- $allPolicies = @() $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies" do { $response = Invoke-MgGraphRequest -Method GET -Uri $uri $responseObj = if ($response -is [string]) { $response | ConvertFrom-Json } else { $response } if ($responseObj.value) { $allPolicies += $responseObj.value } $uri = $responseObj.'@odata.nextLink' } while ($uri) # ---------------------------- # Check if a policy with the same name exists # ---------------------------- $existing = $allPolicies | Where-Object { $_.displayName -eq $PolicyObject.displayName } | Select-Object -First 1 if ($existing) { Write-Host "⚠️ Compliance policy '$($PolicyObject.displayName)' already exists" #Assign-PolicyToGroup -PolicyType "deviceCompliancePolicies" -PolicyId $existing.id -GroupId $GroupId return } # ---------------------------- # Create new compliance policy # ---------------------------- $response = Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies" -Body ($PolicyObject | ConvertTo-Json -Depth 100) -ContentType "application/json" Write-Host "✅ Compliance policy '$($PolicyObject.displayName)' imported" Assign-PolicyToGroup -PolicyType "deviceCompliancePolicies" -PolicyId $response.id -GroupId $GroupId } function Get-OrCreateUpdateRing { param( [hashtable]$Params, [string]$GroupId ) if (-not $Params.displayName) { throw "Update ring must have a displayName" } # ---------------------------- # Fetch all existing update rings via SDK # ---------------------------- $existing = Get-MgDeviceManagementDeviceConfiguration -All | Where-Object { $_.displayName -eq $Params.displayName } | Select-Object -First 1 if ($existing) { Write-Host "⚠️ Update ring '$($Params.displayName)' already exists" Assign-PolicyToGroup -PolicyType "deviceConfigurations" -PolicyId $existing.id -GroupId $GroupId $null = return $existing.id } # ---------------------------- # Create new update ring via SDK # ---------------------------- try { $response = New-MgDeviceManagementDeviceConfiguration -BodyParameter $Params Write-Host "✅ Created update ring '$($Params.displayName)'" Assign-PolicyToGroup -PolicyType "deviceConfigurations" -PolicyId $response.id -GroupId $GroupId $null = return $response.id } catch { Write-Host "❌ Error creating update ring '$($Params.displayName)': $_" return $null } } function Get-OrCreateDriverUpdateProfile { param( [hashtable]$Params, [string]$GroupId ) if (-not $Params.displayName) { throw "Driver update profile must have a displayName" } # ---------------------------- # Fetch all driver update profiles via REST # ---------------------------- $existingProfiles = @() try { $allProfilesRaw = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles" if ($allProfilesRaw -is [string] -and $allProfilesRaw.TrimStart().StartsWith("{")) { # Looks like JSON, parse it $parsed = $allProfilesRaw | ConvertFrom-Json if ($parsed.value) { $existingProfiles = $parsed.value } } elseif ($allProfilesRaw -is [PSCustomObject] -and $allProfilesRaw.value) { $existingProfiles = $allProfilesRaw.value } } catch { Write-Warning "Failed to fetch driver update profiles: $_" } # ---------------------------- # Check if profile already exists # ---------------------------- $existing = $existingProfiles | Where-Object { $_.displayName -eq $Params.displayName } | Select-Object -First 1 if ($existing) { Write-Host "⚠️ Driver update profile '$($Params.displayName)' already exists" if ($GroupId) { Assign-PolicyToGroup -PolicyType "windowsDriverUpdateProfiles" -PolicyId $existing.id -GroupId $GroupId } return $existing.id } # ---------------------------- # Create new driver update profile # ---------------------------- try { $bodyJson = $Params | ConvertTo-Json -Depth 100 $response = Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles" -Body $bodyJson -ContentType "application/json" $profileId = $null if ($response -is [string] -and $response.TrimStart().StartsWith("{")) { $profileId = ($response | ConvertFrom-Json).id } elseif ($response.id) { $profileId = $response.id } Write-Host "✅ Created driver update profile '$($Params.displayName)'" if ($GroupId -and $profileId) { Assign-PolicyToGroup -PolicyType "windowsDriverUpdateProfiles" -PolicyId $profileId -GroupId $GroupId } return $profileId } catch { Write-Error "❌ Error creating driver update profile '$($Params.displayName)': $_" } } # ------------------------- # Groups # ------------------------- $testGroupId = Get-OrCreateGroup -DisplayName "Intune - Test Machine Group" -MailNickname "IntuneTestMachineGroup" $windowsGroupId = Get-OrCreateGroup -DisplayName "Intune - All Windows Workstations" -MailNickname "IntuneWindowsDevices" -DynamicRule '(device.deviceOSType -eq "Windows") and (device.accountEnabled -eq true) and (device.managementType -eq "MDM")' $autopilotGroupId = Get-OrCreateGroup -DisplayName "Intune - AutoPilot Devices" -MailNickname "IntuneAutoPilotDevices" -DynamicRule '(device.devicePhysicalIDs -any (_ -startsWith "[ZTDid]"))' # ------------------------- # Configuration Policies # ------------------------- $policyFiles = Get-ChildItem ./policies/settingscatalog foreach ($policy in $policyFiles) { $null = Import-ConfigurationPolicy -PolicyPath $policy.FullName -GroupId $testGroupId } # ------------------------- # Compliance Policies # ------------------------- $complianceFiles = Get-ChildItem ./policies/compliance foreach ($policy in $complianceFiles) { $null = Import-CompliancePolicy -PolicyPath $policy.FullName -GroupId $testGroupId } # ------------------------- # Windows Update Rings # ------------------------- $updateRingPilot = @{ "@odata.type"= "#microsoft.graph.windowsUpdateForBusinessConfiguration" "displayName"= "Win - Windows Updates - Ring 1 - Pilot" "description"= "Devices in this ring receive updates immediately after release with 1 day grace period before a forced reboot." "automaticUpdateMode"= "windowsDefault" "deliveryOptimizationMode"= "userDefined" "prereleaseFeatures"= "userDefined" "microsoftUpdateServiceAllowed"= $true # Enables updates for Microsoft products "driversExcluded"= $false "qualityUpdatesDeferralPeriodInDays"= 0 "featureUpdatesDeferralPeriodInDays"= 0 "qualityUpdatesPaused"= $false "featureUpdatesPaused"= $false "businessReadyUpdatesOnly"= "userDefined" "skipChecksBeforeRestart"= $false "featureUpdatesRollbackWindowInDays"= 30 "qualityUpdatesWillBeRolledBack"= $false "featureUpdatesWillBeRolledBack"= $false "deadlineForFeatureUpdatesInDays"= 0 "deadlineForQualityUpdatesInDays"= 0 "deadlineGracePeriodInDays"= 1 "postponeRebootUntilAfterDeadline"= $true "autoRestartNotificationDismissal"= "notConfigured" "userPauseAccess"= "disabled" "userWindowsUpdateScanAccess"= "enabled" "updateNotificationLevel"= "defaultNotifications" "allowWindows11Upgrade"= $false "roleScopeTagIds"= @("0") # Scope tags (use appropriate scope tags as needed) "supportsScopeTags"= $true } $null = Get-OrCreateUpdateRing -Params $updateRingPilot $updateRingProd = @{ "@odata.type"= "#microsoft.graph.windowsUpdateForBusinessConfiguration" "displayName"= "Win - Windows Updates - Ring 2 - Production" "description"= "Devices in this ring receive updates 10 days after release with 2-day deadline on install with 1 day grace period before a forced reboot." "automaticUpdateMode"= "windowsDefault" "deliveryOptimizationMode"= "userDefined" "prereleaseFeatures"= "userDefined" "microsoftUpdateServiceAllowed"= $true "driversExcluded"= $false "qualityUpdatesDeferralPeriodInDays"= 10 "featureUpdatesDeferralPeriodInDays"= 0 "qualityUpdatesPaused"= $false "featureUpdatesPaused"= $false "businessReadyUpdatesOnly"= "userDefined" "skipChecksBeforeRestart"= $false "featureUpdatesRollbackWindowInDays"= 30 "qualityUpdatesWillBeRolledBack"= $false "featureUpdatesWillBeRolledBack"= $false "deadlineForFeatureUpdatesInDays"= 2 "deadlineForQualityUpdatesInDays"= 2 "deadlineGracePeriodInDays"= 1 "postponeRebootUntilAfterDeadline"= $true "autoRestartNotificationDismissal"= "notConfigured" "userPauseAccess"= "disabled" "userWindowsUpdateScanAccess"= "enabled" "updateNotificationLevel"= "defaultNotifications" "allowWindows11Upgrade"= $false "roleScopeTagIds"= @("0") "supportsScopeTags"= $true } $null = Get-OrCreateUpdateRing -Params $updateRingProd # ------------------------- # Driver Update Profiles # ------------------------- $driverProfilePilot = @{ "displayName" = "Win - Drivers - Ring 1 - Pilot" "description" = "" "approvalType" = "automatic" "deploymentDeferralInDays" = 0 "newUpdates" = 0 "roleScopeTagIds" = @("0") } $null = Get-OrCreateDriverUpdateProfile -Params $driverProfilePilot $driverProfileProd = @{ "displayName" = "Win - Drivers - Ring 2 - Production" "description" = "" "approvalType" = "automatic" "deploymentDeferralInDays" = 10 "newUpdates" = 0 "roleScopeTagIds" = @("0") } $null = Get-OrCreateDriverUpdateProfile -Params $driverProfileProd # ------------------------- # Disconnect Graph # ------------------------- $null = Disconnect-Graph -ErrorAction SilentlyContinue Write-Host "✅ Script completed"