AutopilotPartner.psm1
function Get-TenantID { # Credit to Daniel Kåven | https://teams.se/powershell-script-find-a-microsoft-365-tenantid/ [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0, HelpMessage="The domain name of the tenant")] [String]$domain ) $request = Invoke-WebRequest -Uri https://login.windows.net/$domain/.well-known/openid-configuration -UseBasicParsing $data = ConvertFrom-Json $request.Content return $Data.token_endpoint.split('/')[3] } function Get-HWID { # Credit to authors of https://www.powershellgallery.com/packages/Get-WindowsAutoPilotInfo/3.9 $bad = $false $session = New-CimSession # Get the common properties. Write-Verbose "Checking $comp" $serial = (Get-CimInstance -CimSession $session -Class Win32_BIOS).SerialNumber # Get the hash (if available) $devDetail = (Get-CimInstance -CimSession $session -Namespace root/cimv2/mdm/dmmap -Class MDM_DevDetail_Ext01 -Filter "InstanceID='Ext' AND ParentID='./DevDetail'") if ($devDetail) { $hash = $devDetail.DeviceHardwareData } else { $bad = $true $hash = "" } # Getting the PKID is generally problematic for anyone other than OEMs, so let's skip it here $product = "" # Depending on the format requested, create the necessary object # Create a pipeline object $c = New-Object psobject -Property @{ "Device Serial Number" = $serial "Windows Product ID" = $product "Hardware Hash" = $hash } # Write the object to the pipeline or array if ($bad) { # Report an error when the hash isn't available Write-Error -Message "Unable to retrieve device hardware data (hash) from computer $comp" -Category DeviceError } Write-Host "Gathered details for device with serial number: $serial" Remove-CimSession $session return $c } function Wait-UntilComplete { # Credit to authors of https://www.powershellgallery.com/packages/Get-WindowsAutoPilotInfo/3.9 param ( [Parameter(Mandatory=$true, Position=0, HelpMessage="Object representing the Autopilot device identity")] $Device ) #Import Check $importStart = Get-Date $percent = 0 $iter = 1 do { $processed = $false $importCheck = Get-AutopilotImportedDevice -id $Device.id if ($importCheck.state.deviceImportStatus -eq "unknown") { Write-Progress -Activity "Autopilot Enrollment" -Status "Awaiting device import..." -PercentComplete $percent $percent += 25 / $iter if ($percent -ge 100) { $percent = 99 } $iter++ Start-Sleep 10 } else { $processed = $true } } while (!$processed) $importDuration = (Get-Date) - $importStart $importSeconds = [Math]::Ceiling($importDuration.TotalSeconds) # Verify that the device imported successfully try { Get-AutopilotDevice -id $importCheck.state.deviceRegistrationId } catch { Write-Error -Message $("The device import failed. Error: " + $_) -ErrorAction Stop } Write-Host "Device imported successfully. Elapsed time to complete import: $importSeconds seconds" # Sync Check $syncStart = Get-Date do { $processed = $false $syncCheck = Get-AutopilotDevice -id $importCheck.state.deviceRegistrationId if (!$syncCheck) { Write-Progress -Activity "Autopilot Enrollment" -Status "Awaiting Intune sync..." Start-Sleep 15 } else { $processed = $true } } while (!$processed) $syncDuration = (Get-Date) - $syncStart $syncSeconds = [Math]::Ceiling($syncDuration.TotalSeconds) Write-Host "Devices synced. Elapsed time to complete sync: $syncSeconds seconds" # Assignment Check $percent = 0 $iter = 1 $assignStart = Get-Date do { $processed = $false $assignCheck = Get-AutopilotDevice -Expand -id $importCheck.state.deviceRegistrationId if (!$assignCheck.deploymentProfileAssignmentStatus.StartsWith("assigned")) { Write-Progress -Activity "Autopilot Enrollment" -Status "Awaiting assignment to a deployment profile... Current Status: $($assignCheck.deploymentProfileAssignmentStatus)" -PercentComplete $percent $percent += 25 / $iter if ($percent -ge 100) { $percent = 99 } $iter++ Start-Sleep 30 } else { $processed = $true } } while (!$processed) $assignDuration = (Get-Date) - $assignStart $assignSeconds = [Math]::Ceiling($assignDuration.TotalSeconds) Write-Host "Profile has been assigned to the device. Elapsed time to complete assignment: $assignSeconds seconds" } function Get-Choice { param ( [Parameter(Mandatory=$true, HelpMessage="Input data")] [Array] $In, [Parameter(Mandatory=$false, HelpMessage="Relevant parameters")] [String[]] $Params, [Parameter(Mandatory=$false, HelpMessage="Allow selection of multiple values")] [switch] $MultipleChoice = $false, [Parameter(Mandatory=$false, HelpMessage="Color the output for better visualization")] [bool] $Color = $true, [Parameter(Mandatory=$false, HelpMessage="Page size")] [int] $PageSize = 8 ) Process { $currentPage = 0 do { for ($i = $currentPage * $pageSize; $i -lt ($currentPage + 1) * $pageSize; $i++) { if ($i -ge $In.Length) { break; } $outStr = $i.ToString() + ". " if ($PSBoundParameters.ContainsKey("Params")) { for ($j = 0; $j -lt $Params.Length; $j++) { $outStr += $In[$i].$($Params[$j]) + " | " } } else { $outStr += $In[$i] } if ($Color) { switch ($i % 2) { 0 { $colorStr = "Yellow" } 1 { $colorStr = "Cyan" } } } else { $colorStr = "White" } Write-Host -ForegroundColor $colorStr $outStr } $selectStr = "Pick the relevant option. " if ($MultipleChoice) { $selectStr += "Select several options by separating via commas. " } if ($pageSize -le $In.Length) { if (($currentPage + 1) * $pageSize -lt $In.Length) { $selectStr += "(N) Next Page " } if ($currentPage -ne 0) { $selectStr += "(P) Previous Page " } } $pick = Read-Host $selectStr switch ($pick) { {$_.ToLower() -eq "n"} { if ((($currentPage + 1) * $pageSize) -gt $In.Length) { Write-Host "Cannot go further: this is the last page." continue } else { $currentPage++ } } {$_.ToLower() -eq "p"} { if ($currentPage -eq 0) { Write-Host "Cannot go back any further: this is the first page." continue } else { $currentPage-- } } default { try { if ($MultipleChoice) { $pickMultiple = $pick.split(",") $choices = foreach ($num in $pickMultiple) { ([int]::parse($num)) } } else { $choices = @([int]::parse($pick)) } } catch { Write-Host "Invalid input." continue } $valid = $true foreach ($option in $choices) { if (!(($option -ge 0) -and ($option -lt $In.Length))) { $valid = $false Write-Host "Invalid option: $($option)" } } if (!$valid) { Write-Host "Invalid input." continue } do { foreach ($option in $choices) { $confStr = "" if ($PSBoundParameters.ContainsKey("Params")) { for ($i = 0; $i -lt $Params.Length; $i++){ $confStr += $In[$option].$($Params[$i]) + " | " } } else { $confStr += $In[$option] } Write-Host $confStr } $Confirmation = Read-Host "Confirm? (y/N)" if (($null -eq $Confirmation) -or ($Confirmation.ToLower() -eq "n")) { break } } while ($Confirmation.ToLower() -ne "y") if ($Confirmation.ToLower() -eq "y") { $choice = foreach ($option in $choices) { $In[$option] } } } } } while ($null -eq $choice) return $choice } } function Find-Tenant { param ( [Parameter(Mandatory=$true)] [String] $ID ) Process { try { $tenantId = Get-TenantID $ID foreach ($tenant in $customers) { if ($tenant.CustomerId -eq $tenantId) { $desiredTenant = $tenant break } } if (!$desiredTenant) { Write-Error -Message "Specified tenant does not have a delegated access relationship in this partner tenant." } } catch { Write-Host "The tenant ID/domain specified was invalid. " Write-Error -Message $_ -Category InvalidArgument } return $desiredTenant } } function Authenticate { # Attempt to authenticate to Partner Center using the provided Partner App ID Write-Host "Initiating interactive sign-in. You will be signing in to Microsoft Partner Center, under the application $($settings.PARTNER_APP_ID)." try { if ($settings.DEVICE_CODE_AUTH) { $partnerToken = New-PartnerAccessToken -ApplicationId $settings.PARTNER_APP_ID -Scopes 'https://api.partnercenter.microsoft.com/user_impersonation' -UseDeviceAuthentication } else { $partnerToken = New-PartnerAccessToken -ApplicationId $settings.PARTNER_APP_ID -Scopes 'https://api.partnercenter.microsoft.com/user_impersonation' -UseAuthorizationCode } $partnerResult = Connect-PartnerCenter -AccessToken $partnerToken.AccessToken } catch { Write-Host "There was a problem signing in to Microsoft Partner Center. Verify your access and the status of Partner Center." Write-Error -Message $_ exit 1 } Write-Host "Connected to Partner Center via $((Get-PartnerOrganizationProfile).CompanyName) | $(Get-TenantID $partnerResult.Account.Tenant)." if (-not $(Get-PartnerMPNProfile).MpnId) { Write-Host "This does not appear to be a Microsoft Partner Network account." -ForegroundColor Red Write-Host "The device will be added directly to the tenant associated with the signed-in account. Ctrl+C to cancel/terminate." -ForegroundColor Red Start-Sleep 5 $isPartner = $false } else { $isPartner = $true } if ($isPartner) { try { $customers = Get-PartnerCustomer } catch { Write-Host "There was a problem getting the list of customers from Microsoft Partner Center. Are you sure you're using your Partner credentials?" exit 1 } # Attain tenant via user input or from parameters. try { if ($PSBoundParameters.ContainsKey("ClientTenant")) { $customer = Find-Tenant $ClientTenant Write-Host "Using tenant $($customer.Name) | $($customer.CustomerId) specified in arguments." } elseif ($settings.FORCE_DEF_TENANT) { $customer = Find-Tenant $settings.DEFAULT_TENANT Write-Host "Using tenant $($customer.Name) | $($customer.CustomerId) specified in settings.json." } else { $customer = Get-Choice -In $customers -Params "Name","Domain","CustomerId" -PageSize 16 } } catch { exit 1 } # Initiation / Verification of the application registration in the tenant. # If an application registration does not exist, one is made using the least privileged permission set. Write-Host "Verifying $($settings.CLIENT_APP_NAME) | $($settings.CLIENT_APP_ID) is an app registration in the client tenant $($customer.Name) | $($customer.CustomerId)" $grant = New-Object -TypeName Microsoft.Store.PartnerCenter.Models.ApplicationConsents.ApplicationGrant $grant.EnterpriseApplicationId = '00000003-0000-0000-c000-000000000000' $grant.Scope = "DeviceManagementManagedDevices.ReadWrite.All,DeviceManagementServiceConfig.ReadWrite.All" try { New-PartnerCustomerApplicationConsent -ApplicationGrants @($grant) -CustomerId $customer.CustomerId -ApplicationId $settings.CLIENT_APP_ID -DisplayName $settings.CLIENT_APP_NAME } catch { if ($_.ErrorDetails.Message -eq ("Permission entry already exists.")) { Write-Host "The application registration already exists in the tenant. Proceeding." -ForegroundColor Yellow } else { Write-Host "An unknown error occurred verifying the app registration's presence in the tenant." exit 1 } } # Authenticate to Microsoft Graph using the application ID and previously instantiated credentials. Write-Host "Authenticating to tenant $($customer.Name) | $($customer.CustomerId) through Microsoft Partner Network using the app registration $($settings.CLIENT_APP_NAME) | $($settings.CLIENT_APP_ID)" if ($settings.CLIENT_APP_ID -eq $settings.PARTNER_APP_ID) { $authReq = New-PartnerAccessToken -ApplicationId $settings.CLIENT_APP_ID -RefreshToken $partnerToken.RefreshToken -Scopes "https://graph.microsoft.com/.default" -Tenant $customer.CustomerId } else { Write-Host "The application ID for the partner application is different than the client application. Re-authentication is required." if ($settings.DEVICE_CODE_AUTH) { $authReq = New-PartnerAccessToken -ApplicationId $settings.CLIENT_APP_ID -Scopes "https://graph.microsoft.com/.default" -Tenant $customer.CustomerId -UseDeviceAuthentication } else { $authReq = New-PartnerAccessToken -ApplicationId $settings.CLIENT_APP_ID -Scopes "https://graph.microsoft.com/.default" -Tenant $customer.CustomerId -UseAuthorizationCode } } } else { if ($settings.DEVICE_CODE_AUTH) { $authReq = New-PartnerAccessToken -ApplicationId $settings.CLIENT_APP_ID -Scopes "https://graph.microsoft.com/.default" -Tenant Get-TenantID $partnerResult.Account.Tenant -UseDeviceAuthentication } else { $authReq = New-PartnerAccessToken -ApplicationId $settings.CLIENT_APP_ID -Scopes "https://graph.microsoft.com/.default" -Tenant $customer.CustomerId -UseAuthorizationCode } } $token = ConvertTo-SecureString -Force -AsPlainText $authReq.AccessToken return $token } function Import-Autopilot { param ( [Parameter(Mandatory=$false)] [String] $SettingsPath = '.\settings.json', [Parameter(Mandatory=$false)] [String] $GroupTag, [Parameter(Mandatory=$false)] [String] $ClientTenant ) # Get settings configuration from settings.json try { $settings = Get-Content -Path $SettingsPath } catch { Write-Error -Message "Error locating settings.json file. Please be sure that -SettingsPath is correct." } $settings = $settings -replace '(?m)(?<=^([^"]|"[^"]*")*)//.*' -replace '(?ms)/\*.*?\*/' $settings = $settings | ConvertFrom-Json # Check if required values are present in settings.json if (!$settings.PARTNER_APP_ID -or !$settings.CLIENT_APP_ID -or !$settings.CLIENT_APP_NAME) { Write-Error -Message "One of the following required values is missing from settings.json: PARNTER_APP_ID, CLIENT_APP_ID, CLIENT_APP_NAME" exit 1 } # Attempt to authenticate to Partner Center using the provided Partner App ID Write-Host "Initiating interactive sign-in. You will be signing in to Microsoft Partner Center, under the application $($settings.PARTNER_APP_ID)." try { if ($settings.DEVICE_CODE_AUTH) { $partnerToken = New-PartnerAccessToken -ApplicationId $settings.PARTNER_APP_ID -Scopes 'https://api.partnercenter.microsoft.com/user_impersonation' -UseDeviceAuthentication } else { $partnerToken = New-PartnerAccessToken -ApplicationId $settings.PARTNER_APP_ID -Scopes 'https://api.partnercenter.microsoft.com/user_impersonation' -UseAuthorizationCode } $partnerResult = Connect-PartnerCenter -AccessToken $partnerToken.AccessToken } catch { Write-Host "There was a problem signing in to Microsoft Partner Center. Verify your access and the status of Partner Center." Write-Error -Message $_ exit 1 } Write-Host "Connected to Partner Center via $((Get-PartnerOrganizationProfile).CompanyName) | $(Get-TenantID $partnerResult.Account.Tenant)." try { $customers = Get-PartnerCustomer } catch { Write-Host "There was a problem getting the list of customers from Microsoft Partner Center. Are you sure you're using your Partner credentials?" exit 1 } # Attain tenant via user input or from parameters. try { if ($PSBoundParameters.ContainsKey("ClientTenant")) { $customer = Find-Tenant $ClientTenant Write-Host "Using tenant $($customer.Name) | $($customer.CustomerId) specified in arguments." } elseif ($settings.FORCE_DEF_TENANT) { $customer = Find-Tenant $settings.DEFAULT_TENANT Write-Host "Using tenant $($customer.Name) | $($customer.CustomerId) specified in settings.json." } else { $customer = Get-Choice -In $customers -Params "Name","Domain","CustomerId" -PageSize 16 } } catch { exit 1 } # Attain group tag if ($PSBoundParameters.ContainsKey("GroupTag")) { Write-Host "Using group tag $($GroupTag) as specified in arguments." } elseif ($settings.FORCE_DEF_GROUP_TAG) { $GroupTag = $settings.DEFAULT_GROUP_TAG Write-Host "Using group tag '$($settings.DEFAULT_GROUP_TAG) as specified in settings.json" } else { do { $GroupTag = Read-Host -Prompt "Enter the group tag of the device (Default: '$($settings.DEFAULT_GROUP_TAG)')" if (!$GroupTag) { $GroupTag = $DEFAULT_GROUP_TAG break } else { do { $Confirmation = Read-Host -Prompt "Group Tag: '$($GroupTag)' | Correct? (y/N)" } while (!(($Confirmation.ToLower() -eq "y" ) -or ($Confirmation.ToLower() -eq "n") -or (!$Confirmation))) } } while ($Confirmation.ToLower() -ne "y") } # Attain user information if ENABLE_ASSIGN_USER is true. if ($settings.ENABLE_ASSIGN_USER) { do { $AssignedUser = Read-Host -Prompt "Enter the UPN of the assigned user of the device (Press enter if none)" if (!$AssignedUser) { break } else { do { $Confirmation = Read-Host -Prompt "Assigned User: '$($AssignedUser)' | Correct? (y/N)" } while (!(($Confirmation.ToLower() -eq "y" ) -or ($Confirmation.ToLower() -eq "n") -or (!$Confirmation))) } } while ($Confirmation.ToLower() -ne "y") } else { Write-Host "User assignment disabled - skipping user assignment" } # Initiation / Verification of the application registration in the tenant. # If an application registration does not exist, one is made using the least privileged permission set. Write-Host "Verifying $($settings.CLIENT_APP_NAME) | $($settings.CLIENT_APP_ID) is an app registration in the client tenant $($customer.Name) | $($customer.CustomerId)" $grant = New-Object -TypeName Microsoft.Store.PartnerCenter.Models.ApplicationConsents.ApplicationGrant $grant.EnterpriseApplicationId = '00000003-0000-0000-c000-000000000000' $grant.Scope = "DeviceManagementManagedDevices.ReadWrite.All,DeviceManagementServiceConfig.ReadWrite.All" try { New-PartnerCustomerApplicationConsent -ApplicationGrants @($grant) -CustomerId $customer.CustomerId -ApplicationId $settings.CLIENT_APP_ID -DisplayName $settings.CLIENT_APP_NAME } catch { if ($_.ErrorDetails.Message -eq ("Permission entry already exists.")) { Write-Host "The application registration already exists in the tenant. Proceeding." -ForegroundColor Yellow } else { Write-Host "An unknown error occurred verifying the app registration's presence in the tenant." exit 1 } } # Authenticate to Microsoft Graph using the application ID and previously instantiated credentials. Write-Host "Authenticating to tenant $($customer.Name) | $($customer.CustomerId) through Microsoft Partner Network using the app registration $($settings.CLIENT_APP_NAME) | $($settings.CLIENT_APP_ID)" if ($settings.CLIENT_APP_ID -eq $settings.PARTNER_APP_ID) { $authReq = New-PartnerAccessToken -ApplicationId $settings.CLIENT_APP_ID -RefreshToken $partnerToken.RefreshToken -Scopes "https://graph.microsoft.com/.default" -Tenant $customer.CustomerId } else { Write-Host "The application ID for the partner application is different than the client application. Re-authentication is required." if ($settings.DEVICE_CODE_AUTH) { $authReq = New-PartnerAccessToken -ApplicationId $settings.CLIENT_APP_ID -Scopes "https://graph.microsoft.com/.default" -Tenant $customer.CustomerId -UseDeviceAuthentication } else { $authReq = New-PartnerAccessToken -ApplicationId $settings.CLIENT_APP_ID -Scopes "https://graph.microsoft.com/.default" -Tenant $customer.CustomerId -UseAuthorizationCode } } $token = ConvertTo-SecureString -Force -AsPlainText $authReq.AccessToken # Initiate enrollment to Autopilot. Write-Host "Initiating Autopilot Enrollment..." Write-Host "Connecting to Microsoft Graph..." Connect-MgGraph -AccessToken $token if (!(("DeviceManagementManagedDevices.ReadWrite.All" -in $(Get-MgContext).scopes) -and ("DeviceManagementServiceConfig.ReadWrite.All" -in $(Get-MgContext).scopes))) { Write-Host "The application does not have the correct scopes. Please review the enterprise application $($settings.CLIENT_APP_NAME) | $($settings.CLIENT_APP_ID)" Write-Host "Scopes assigned: $($(Get-MgContext).scopes)" exit 1 } Write-Host "Acquiring hardware hash information..." $device = Get-HWID $importIdentity = Add-AutopilotImportedDevice -serialNumber $device."Device Serial Number" -hardwareIdentifier $device."Hardware Hash" -groupTag $GroupTag -assignedUser $AssignedUser Wait-UntilComplete -Device $importIdentity # Remove tracks. This may not work properly (it is what it is). Write-Host "Disconnecting from Microsoft services..." Disconnect-MgGraph Disconnect-PartnerCenter Write-Host "Removing all installed modules..." Write-Progress -Activity "Removing installed modules" -Status "Removing PartnerCenter" -PercentComplete 0 Remove-Module -Name PartnerCenter -Force Write-Progress -Activity "Removing installed modules" -Status "Removing WindowsAutoPilotIntune" -PercentComplete 25 Remove-Module -Name WindowsAutoPilotIntune -Force Write-Progress -Activity "Removing installed modules" -Status "Uninstalling PartnerCenter" -PercentComplete 50 Uninstall-Module -Name PartnerCenter -Force Write-Progress -Activity "Removing installed modules" -Status "Uninstalling WindowsAutoPilotIntune" -PercentComplete 75 Uninstall-Module -Name WindowsAutoPilotIntune -Force # Wait for soak. Write-Host "Enrollment complete. Timer to allow Intune to soak begins now." if (!$settings.SOAK_TIME){ $soakSecs = 300 } else { $soakSecs = $settings.SOAK_TIME } for ($i = 0; $i -le $soakSecs; $i++) { Write-Progress -Activity "Awaiting Soak" -SecondsRemaining $($soakSecs - $i) -PercentComplete $($i / $soakSecs) Start-Sleep 1 } exit 0 } |