diff --git a/ImportPolicies.ps1 b/ImportPolicies.ps1 index a1ac0b4..dc000a9 100644 --- a/ImportPolicies.ps1 +++ b/ImportPolicies.ps1 @@ -1,157 +1,319 @@ -# Check if the Microsoft Graph PowerShell SDK is installed -if (-not (Get-Module -ListAvailable -Name Microsoft.Graph)) { - Install-Module -Name Microsoft.Graph -Scope CurrentUser -Force -} +# ========================================== +# Intune / Graph Automation Script - Idempotent +# ========================================== -# Check if the Microsoft Graph PowerShell SDK is installed -if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Beta)) { - Install-Module -Name Microsoft.Graph.Beta -Scope CurrentUser -Force +# 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 +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 -# Create the security group with static membership -$groupBody = @{ - displayName = "Intune - Test Machine Group" - mailEnabled = $false - mailNickname = "IntuneTestMachineGroup" - securityEnabled = $true - # No need for groupTypes or membershipRule for static groups +# ------------------------- +# 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 } -$group = $groupBody.displayName +function Import-ConfigurationPolicy { + param( + [string]$PolicyPath, + [string]$GroupId + ) -# Convert the body to JSON -$groupBodyJson = $groupBody | ConvertTo-Json -Depth 100 - -# Create the static group using Invoke-MgGraphRequest -$response = Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/beta/groups" -Body $groupBodyJson -ContentType "application/json" - -$groupId = $response.id - -Write-Host "✅ Successfully created static group '$group - $groupid'" - -# Define the dynamic membership rule -$dynamicRule = '(device.deviceOSType -eq "Windows") and (device.accountEnabled -eq true) and (device.managementType -eq "MDM")' -#(device.devicePhysicalIDs -any (_ -startsWith "[ZTDid]")) - - -# Create the security group with dynamic membership -$groupBody = @{ - displayName = "Intune - All Windows Workstations" - mailEnabled = $false - mailNickname = "IntuneWindowsDevices" - securityEnabled = $true - groupTypes = @("DynamicMembership") - membershipRule = $dynamicRule - membershipRuleProcessingState = "On" -} - -$group = $groupBody.displayname - -# Convert the body to JSON -$groupBodyJson = $groupBody | ConvertTo-Json -Depth 100 - -# Create the group using Invoke-MgGraphRequest -$null = Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/beta/groups" -Body $groupBodyJson -ContentType "application/json" -Write-Host "✅ Successfully created group $group" - -# Define the dynamic membership rule -$dynamicRule = '(device.devicePhysicalIDs -any (_ -startsWith "[ZTDid]"))' - - -# Create the security group with dynamic membership -$groupBody = @{ - displayName = "Intune - AutoPilot Devices" - mailEnabled = $false - mailNickname = "IntuneWindowsDevices" - securityEnabled = $true - groupTypes = @("DynamicMembership") - membershipRule = $dynamicRule - membershipRuleProcessingState = "On" -} - -$group = $groupBody.displayname - -# Convert the body to JSON -$groupBodyJson = $groupBody | ConvertTo-Json -Depth 100 - -# Create the group using Invoke-MgGraphRequest -$null = Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/beta/groups" -Body $groupBodyJson -ContentType "application/json" -Write-Host "✅ Successfully created group $group" - -$policies = Get-ChildItem ./policies/settingscatalog - -ForEach ($policy in $policies) { - $PolicyName = $policy.name - - $JsonData = Get-Content -Path ./policies/settingscatalog/$PolicyName -Raw + # Load the JSON policy + $JsonData = Get-Content -Path $PolicyPath -Raw $JsonDataUpdated = $JsonData -replace '\$tenantId', $tenantId - $PolicyObject = $JsonDataUpdated | ConvertFrom-Json + $PolicyObject = $JsonDataUpdated | ConvertFrom-Json - try { + if (-not $PolicyObject.name) { + throw "Policy has no 'name': $PolicyPath" + } - # Add assignment to the created group - $uri = "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies" # Using the beta version - $response = Invoke-MgGraphRequest -Method POST -Uri $uri -Body ($PolicyObject | ConvertTo-Json -Depth 100) - Write-Host "✅ $PolicyName - successfully imported!" + # ---------------------------- + # Fetch all existing configuration policies + # ---------------------------- + $allPolicies = @() + $uri = "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies" - $policyid = $response.id + 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) - $assignmentUri = "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies/$PolicyId/assign" - $assignmentBody = @{ - assignments = @( - @{ - target = @{ - "@odata.type" = "#microsoft.graph.groupAssignmentTarget" - groupId = $groupId - } + # ---------------------------- + # 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 $assignmentUri -Body ($assignmentBody | ConvertTo-Json -Depth 100) - Write-Host "✅ Group assigned to the policy!" - - } catch { - Write-Error "❌ An error occurred while importing the policy: $_" + } + ) } + Invoke-MgGraphRequest -Method POST -Uri $uri -Body ($body | ConvertTo-Json -Depth 100) + Write-Host "✅ Group assigned to $PolicyType policy ID $PolicyId" } -$policies = Get-ChildItem ./policies/compliance +function Import-CompliancePolicy { + param( + [string]$PolicyPath, + [string]$GroupId + ) -ForEach ($policy in $policies) { - $PolicyName = $policy.name - - $JsonData = Get-Content -Path ./policies/compliance/$PolicyName -Raw + $JsonData = Get-Content -Path $PolicyPath -Raw $JsonDataUpdated = $JsonData -replace '\$tenantId', $tenantId - $PolicyObject = $JsonDataUpdated | ConvertFrom-Json + $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 { - $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies" # Using the beta version - $null = Invoke-MgGraphRequest -Method POST -Uri $uri -Body ($PolicyObject | ConvertTo-Json -Depth 100) - Write-Host "✅ $PolicyName - successfully imported!" + $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-Error "❌ An error occurred while importing the policy: $_" + 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)': $_" } } -# Create Windows Update Ring Policies -# Create a baseline policy using web interface -# Extract the JSON Data to build paramters -# - Get-MgDeviceManagementDeviceConfiguration | Select-Object displayName, id, @{Name="JSON"; Expression={ $_ | ConvertTo-Json -Depth 10 }} -# Get the ID of the policy you created and get the JSON structure -# - Get-MgDeviceManagementDeviceConfiguration -DeviceConfigurationId "" | ConvertTo-Json -Depth 10 -# Define the update ring configuration with Microsoft product updates enabled -$params = @{ + + + + + +# ------------------------- +# 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." @@ -182,16 +344,17 @@ $params = @{ "supportsScopeTags"= $true } -$ring = $params.displayName +$null = Get-OrCreateUpdateRing -Params $updateRingPilot -$params = @{ + + +$updateRingProd = @{ "@odata.type"= "#microsoft.graph.windowsUpdateForBusinessConfiguration" "displayName"= "Win - Windows Updates - Ring 2 - Production" - "description"= "Devices in this ring receive updates 10 days after release and have a 2-day deadline on install with 1 day grace period before a forced reboot." - "version"= 1 + "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" - "automaticUpdateMode"= "windowsDefault" "microsoftUpdateServiceAllowed"= $true "driversExcluded"= $false "qualityUpdatesDeferralPeriodInDays"= 10 @@ -214,52 +377,38 @@ $params = @{ "allowWindows11Upgrade"= $false "roleScopeTagIds"= @("0") "supportsScopeTags"= $true - "createdDateTime"= "2023-10-27T15:13:33.5897267Z" - "lastModifiedDateTime"= "2023-10-27T15:13:33.5897267Z" } - -$ring = $params.displayName - -# Create the update ring policy in Intune -$null = New-MgDeviceManagementDeviceConfiguration -BodyParameter $params -Write-Host "✅ Successfully created $ring" +$null = Get-OrCreateUpdateRing -Params $updateRingProd -$uri = "https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles" -# Define the JSON body for the new driver update profile -$body = @{ +# ------------------------- +# Driver Update Profiles +# ------------------------- +$driverProfilePilot = @{ "displayName" = "Win - Drivers - Ring 1 - Pilot" - "description" = "" # Empty description field from original JSON - "approvalType" = "automatic" # "automatic" from the original JSON - "deploymentDeferralInDays" = 0 # "0" from the original JSON - "newUpdates" = 0 # "0" from the original JSON - "roleScopeTagIds" = @("0") # Role Scope Tag ID from the original JSON + "description" = "" + "approvalType" = "automatic" + "deploymentDeferralInDays" = 0 + "newUpdates" = 0 + "roleScopeTagIds" = @("0") } +$null = Get-OrCreateDriverUpdateProfile -Params $driverProfilePilot -$ring = $body.displayName -$groupBodyJson = $Body | ConvertTo-Json -Depth 100 -# Send the POST request to create the Driver Update Profile -$null = Invoke-MgGraphRequest -Method POST -Uri $uri -Body $groupBodyJson -ContentType "application/json" -Write-Host "✅ Successfully created group $ring" -$uri = "https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles" - -# Define the JSON body for the new driver update profile -$body = @{ +$driverProfileProd = @{ "displayName" = "Win - Drivers - Ring 2 - Production" - "description" = "" # Empty description field from original JSON - "approvalType" = "automatic" # "automatic" from the original JSON - "deploymentDeferralInDays" = 10 # "10" from the original JSON - "newUpdates" = 0 # "0" from the original JSON - "roleScopeTagIds" = @("0") # Role Scope Tag ID from the original JSON + "description" = "" + "approvalType" = "automatic" + "deploymentDeferralInDays" = 10 + "newUpdates" = 0 + "roleScopeTagIds" = @("0") } -$ring = $body.displayName -$groupBodyJson = $Body | ConvertTo-Json -Depth 100 +$null = Get-OrCreateDriverUpdateProfile -Params $driverProfileProd -# Send the POST request to create the Driver Update Profile -$null = Invoke-MgGraphRequest -Method POST -Uri $uri -Body $groupBodyJson -ContentType "application/json" -Write-Host "✅ Successfully created group $ring" - -$null = Disconnect-Graph -ErrorAction SilentlyContinue \ No newline at end of file +# ------------------------- +# Disconnect Graph +# ------------------------- +$null = Disconnect-Graph -ErrorAction SilentlyContinue +Write-Host "✅ Script completed"