Demo-Autopilot.ps1
<#PSScriptInfo .VERSION 1.0.0.2 .GUID bb30a59d-b328-44da-9aa3-6f766d4e4f12 .AUTHOR Frits van Drie .COMPANYNAME 3-Link Opleidingen (3-link.nl) .COPYRIGHT (c) 2023 3-Link bv (NL). Anyone is free to use and distribute this module freely without modification. If you like this or experience failures, please notify me .TAGS Demoscript .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES microsoft.graph.authentication .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES 1.0.0.0 (2024-09-06) Initial release 1.0.0.1 (2024-09-06) Removed Break 1.0.0.2 (2024-09-06) Minor updates in Comment-Based Help .PRIVATEDATA #> <# .SYNOPSIS Full setup for demonstrating Autopilot with 'Entra Join' or 'Hybrid Join' .DESCRIPTION This script will setup all components for Autopilot (optionally with Hybrid Join) in a demo environment. .PARAMETER CsvPath The full path to the csv-file for importing Autopilot devices. The file may contain one or multiple devices and can be generated with 'Get-WindowsAutopilotInfo.ps1' .PARAMETER TenantPrefix The prefix of the Entra tenant. The prefix will be appended with 'onmicrosoft.com' .PARAMETER HybridJoin [switch] If not used, a standard Autopilot deployment will be created. If this switch is used a Hybrid-Join deployment will be created. .PARAMETER SyncedOU Optional parameter to specify the distinguishedName of the Organization Unit that is synchronized to Entra ID by 'Entra Connect'. If not specified the path is determined from the Entra Connect configuration. .PARAMETER OdjConnectorServer The ComputerName of the server where 'Entra Connect' and the 'Intune Offline Domain Join (ODJ) Connector' are installed. .PARAMETER AssignUser [switch] When used a demo user account will be created in Entra ID. The user will be assigned to all imported devices. The user will take one license for EMS. .EXAMPLE .\Demo-Autopilot.ps1 -CsvPath Import.csv -TenantPrefix LODSM123456 This will import the device(s) in the csv-file for LODSM23456.onmicrosoft.com and configure it for an Entra joined Autopilot .EXAMPLE .\Demo-Autopilot.ps1 -CsvPath Import.csv -OdjConnectorServer SEA-SVR1 -TenantPrefix LODSM123456 -AssignUser -HybridJoin -SyncedOU 'OU=Devices,OU=ENTRA,DC=contoso,DC=com' This will import the device(s) in the csv-file for LODSM123456.onmicrosoft.com for Hybrid Join Autopilot. The parameter -SyncedOU refers to the OU that is synchronized with 'Entra Connect' The device(s) will be assigned to a newly created Entra user. .NOTES Author : Frits van Drie (f.vandrie) Company : 3-Link Opleidingen (3-Link.nl) This script is developed for training and demonstration only. Do not run this script in a production environment. A perfect way to run this script is to open it in your favorite PowerShell editor and run each region separate in sequence. If no LDAP path is specified for the service accounts, the default location for new Users will be used If no LDAP path is specified for the computer accounts, it will be extracted from the Entra Connect configuration. The creation of a service account is optional and the ODJ Server system account can be used instead. Prerequisites for this demo: . Global Administrator access to a Microsoft Entra ID tenant . A valid subscription for 'EMS Enterprise E3 or E5' . Domain Administrator access to an on-premise AD Domain . Entra Connect in place and configured for Hybrid Join . An AD domain controller configured for WinRM . An AD domain joined server (WS 2016 or later) configured for WinRM . An AD domain joined computer (Win10 or later) for running this script . A test computer or VM for registration with Autopilot in OOBE phase. Windows 10 or later, at least 4GB memory and 4 (v)CPU's . A Csv-file containing the hardware info of the test computer for import into Autopilot (Get-WindowsAutopilotInfo.ps1) . Access to internet for all computers . PowerShell 5.1, 7+ This script will create or install: . An AD user for the OdjConnectorSvc. The user will be an Administrator on the ODJ Server and will be delegated to create computer objects in AD . An AD group for the ODJ Server . Change the local security policy on the ODJ Server that will allow the user to logon as a service . An Entra dynamic group for all users with a EMS license . An Entra dynamic group for all standard Autopilot devices . An Entra dynamic group for all Hybrid Join Autopilot devices . An Intune Enrollment profile for Autopilot devices . An Intune Enrollment Status Page (ESP) for Autopilot devices . An Intune Device Configuration profile for skipping ESP User part . An Intune Device Configuration profile for joining an AD domain . Import a CSV file into Autopilot with one or more devices . Optional: An Entra ID user account for assigning to the device If the variable $assignUser is $true, an Entra user will be created and assigned an EMS license. This user will be assigned to the Autopilot device(s). If $assignUser is declared $false, no user will be assigned. . If this script is run multiple times, most creations from previous runs will be removed and recreated. #> [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High",DefaultParameterSetName="Standard")] param( [Parameter(Mandatory=$false,ParameterSetName='Standard')] [Parameter(Mandatory=$false,ParameterSetName='HybridJoin')] [string]$CsvPath = $CsvPath, [Parameter(Mandatory=$false,ParameterSetName='Standard')] [Parameter(Mandatory=$false,ParameterSetName='HybridJoin')] [string]$TenantPrefix = $TenantPrefix, [Parameter(Mandatory=$false,ParameterSetName='HybridJoin')] [switch]$HybridJoin = $HybridJoin, [Parameter(Mandatory=$false,ParameterSetName='HybridJoin')] [string]$SyncedOU = $SyncedOU, [Parameter(Mandatory=$false,ParameterSetName='HybridJoin')] [string]$OdjConnectorServer = $OdjConnectorServer, [Parameter(Mandatory=$false,ParameterSetName='Standard')] [Parameter(Mandatory=$false,ParameterSetName='HybridJoin')] [switch]$AssignUser = $AssignUser ) #Requires -RunAsAdministrator #region: Requirements $invocationInfo = $myInvocation.InvocationName $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent() $windowsPrincipal = New-Object System.Security.Principal.WindowsPrincipal($currentUser) Write-Host "Requirements" -f Cyan Write-Host "`tScript: $invocationInfo" Write-Host "`tCurrent user: $($currentUser.Name)" Write-Host "`tDomain Admin: " -NoNewline if ($windowsPrincipal.IsInRole("Domain Admins")) { Write-Host "true" -f Green } else { Write-Host "false " -f Red Write-Host "`tThis script requires 'Domain Admins' membership and elevated permissions" -f Yellow exit } Write-Host #endregion #region: Variables Write-Host "Setup: Variables" -f Cyan Write-Host "`tVariables" $startTime = Get-Date if ($HybridJoin) { $autopilotType = 'HybridJoin' } else { $autopilotType = 'Standard' } if ( -not $tenantPrefix) { $tenantPrefix = Read-Host "Tenantname prefix" } #DEL if ( -not $tenantAdminPassPlain) { $tenantAdminPassPlain = Read-Host "Tenant admin password" } $tenantDnsName = "$tenantPrefix.onmicrosoft.com" $tenantId = $tenantDnsName #DEL $tenantAdminUPN = "Admin@$tenantDnsName" $domainDnsName = (Get-CimInstance -ClassName Win32_ComputerSystem).domain $domainNBName = $domainDnsName.Split('.')[0].toUpper() $domainDN = "DC=$($domainDnsName.Split('.') -join ',DC=')" $domainControllerName = (Get-WmiObject -Namespace 'root\directory\ldap' -Class 'ds_computer' | Where-Object {$_.ds_useraccountcontrol -eq 532480} ).DS_dNSHostName if ($HybridJoin) { $autopilotGroupName = 'Autopilot Hybrid-Join Devices' $orderID = 'HybridJoin' $odjGroupName = 'ODJ Connector Servers' $odjGroupDescription = 'Servers running AD-ODJ-Connector for Autopilot Hybrid Join' $odjSvcName = 'ODJConnectorSvc' $odjSvcAccountName = "IntuneODJ_$odjConnectorServer" $odjSvcAccountPass = 'Pa55w.rd' $ldapPathUsers = (([adsisearcher]'(&(objectclass=domain))').FindOne().properties.wellknownobjects | Where-Object {$_ -match '^B:32:A9D1CA15768811D1ADED00C04FD8D5CD:(.*)$'}).split(':')[-1] $ldapPathComputers = (([adsisearcher]'(&(objectclass=domain))').FindOne().properties.wellknownobjects | Where-Object {$_ -match '^B:32:AA312825768811D1ADED00C04FD8D5CD:(.*)$'}).split(':')[-1] $ldapPathGroups = $ldapPathUsers $ldapPathSvcAcct = $ldapPathUsers $devicePolicyDJName = "Hybrid Join Domain $domainNBName" $devicePolicyDJDescr = "Demo Policy for $devicePolicyDJName" $enrollmentType = '#microsoft.graph.activeDirectoryWindowsAutopilotDeploymentProfile' # Hybrid-Joined $enrollmentProfileName = 'Autopilot with Hybrid Join' $enrollmentProfileDescr = "Demo enrollment profile for $enrollmentProfileName" } else { $autopilotGroupName = 'Autopilot Devices' $orderID = 'Standard' $enrollmentType = '#microsoft.graph.azureADWindowsAutopilotDeploymentProfile' # Entra ID joined $enrollmentProfileName = 'Autopilot' $enrollmentProfileDescr = "Demo enrollment profile for $enrollmentProfileName" } $apMembershipRule = "(device.devicePhysicalIds -any (_ -contains `"[ZTDId]`")) and (device.devicePhysicalIds -any (_ -eq `"[OrderID]:$orderID`"))" $mdmUsersGroupName = 'Intune Users' $intuneProductId = 'c1ec4a95-1f05-45b3-a911-aa3fa01094f5' $mdmMembershipRule = "(user.assignedPlans -any (assignedPlan.servicePlanId -eq `"$intuneProductId`" -and assignedPlan.capabilityStatus -eq `"Enabled`"))" $devicePolicyESP = 'Disable User ESP' $espDisplayName = "ESP $enrollmentProfileName" $assignedUserGivenName = 'Ademo' $assignedUserSurName = 'Youser' $assignedUserDisplayName = "$assignedUserGivenName $assignedUserSurName" $assignedUserUpn = "$assignedUserGivenName@$tenantDnsName" $assignedUserPassPlain = 'DemoY0user' if (-not $assignUser) { Remove-Variable "assignedUser*" -Force -ErrorAction SilentlyContinue } #endregion #region: Packages Write-Host "Setup: Packages" -f Cyan # Package provider $packageProviderName = 'NuGet' $ppMinimalVersion = '2.8.5.201' Write-Host "`tPackage provider" Write-Host "`t`tProvider Name : $packageProviderName" Write-Host "`t`tMinimal Version: $ppMinimalVersion`t" -NoNewline try { $packageProvider = Get-PackageProvider -Name $packageproviderName -ForceBootstrap -ErrorAction Stop | Where Version -ge $minimalVersion | Sort Version if ($packageProvider) { Write-Host 'present' -f Green Write-Host "`t`tCurrent version: $($packageProvider.Version)" } else { Write-Host "failed" -f Red } } catch { throw $_ } $repoName = 'PSGallery' try { Write-Host "`tGet Repository" Write-Host "`t`tName : $repoName`t" -NoNewline $repo = Get-PSRepository -Name $repoName -ErrorAction Stop Write-Host "present" -f Yellow if ($repo.InstallationPolicy -ne 'Trusted') { $color = 'Yellow' } else { $color = 'Green' } Write-Host "`t`tPolicy: $($repo.InstallationPolicy)" -f $color } catch { Write-Host "failed" -f Red try { Write-Host "`tRegister Repository" Write-Host "`t`tName : $repoName`t" -NoNewline $repo = Register-PSRepository -Default -ErrorAction Stop Write-Host 'success' -f Green } catch { Write-Host 'failed' -f Red throw $_ } } if ($repo.InstallationPolicy -ne 'Trusted') { try { Write-Host "`tTrust Repository" Write-Host "`t`tName : $repoName" Set-PSRepository -Name $repoName -InstallationPolicy Trusted -ErrorAction Stop $repo = Get-PSRepository -Name $repoName -ErrorAction Stop Write-Host "`t`tPolicy: $($repo.InstallationPolicy)" -f Green } catch { Write-Host 'failed' -f Red throw $_ } } #endregion #region: Modules Write-Host "Setup: Modules" -f Cyan $modules = @( 'Microsoft.Graph.Authentication' ) Write-Host "`tImport Module" $missingModule = $false foreach ($module in $modules) { try { Write-Host "`t`t$module`t" -NoNewline Import-Module -Name $module -ErrorAction Stop Write-Host "present" -f Green } catch { Write-Host "installing`t" -NoNewline -f yellow try { Install-Module $module -Repository PSGallery -ErrorAction Stop Write-Host "success" -f Green } catch { Write-Host "failed" -f Red $missingModule = $true } } } if ($missingModule) { throw 'Missing one or more required modules' } #endregion #region: Functions Write-Host "Setup: Functions" -f Cyan Write-Host "`tGet-3gIntuneDeviceConfigurationProfile{}" Function Get-3gIntuneDeviceConfigurationProfile { <# .NOTES Name: Get-3gIntuneDeviceConfigurationProfile Author: Frits van Drie (3-Link.nl) Date: 2024-06-18 #> [CmdletBinding()] param() $batch = @' { "requests":[ { "id":"/deviceManagement/configurationPolicies", "method":"GET", "url":"/deviceManagement/configurationPolicies?$top=1000&$select=id,name,lastModifiedDateTime,roleScopeTagIds,createdDateTime&$orderBy=name asc" }, { "id":"/deviceManagement/deviceConfigurations", "method":"GET", "url":"/deviceManagement/deviceConfigurations?$top=1000&$select=id,displayName,lastModifiedDateTime,roleScopeTagIds,createdDateTime&$orderBy=displayName asc" }, { "id":"/deviceManagement/groupPolicyConfigurations", "method":"GET", "url":"/deviceManagement/groupPolicyConfigurations?$top=1500" }, { "id":"/deviceAppManagement/mobileAppConfigurations", "method":"GET", "url":"/deviceAppManagement/mobileAppConfigurations?$top=1000&$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig eq true" }, { "id":"/deviceManagement/resourceAccessProfiles", "method":"GET", "url":"/deviceManagement/resourceAccessProfiles?$top=1000" } ] } '@ #$batch | ConvertFrom-Json $uri = 'https://graph.microsoft.com/beta/$batch' Write-Verbose "Invoke-MgGraphRequest" Write-Verbose "`tUri : '$uri'" Write-Verbose "`tMethod: POST" $restResult = (Invoke-MgGraphRequest -Method POST -Uri $uri -Body $batch -OutputType PSObject -ErrorAction Stop) #.value foreach ($response in $restResult.responses) { foreach ($item in $response.body.value) { $ht = @{ '@odata.type' = $item.'@odata.type' id = $item.id createdDateTime = $item.createdDateTime lastModifiedDateTime = $item.lastModifiedDateTime roleScopeTagIds = $item.roleScopeTagIds resourceType = $response.id } if ($item.displayName) { $ht.Add('displayName', $item.displayName) } elseif ($item.profileName) { $ht.Add('displayName', $item.profileName) } elseif ($item.name) { $ht.Add('displayName', $item.name) } else { $ht.Add('displayName', '') } $objOut = [PSCustomObject]$ht Write-Verbose ("Output: {0} [{1}] " -f ($objOut.displayName).PadRight(60), $objOut.id) Write-Output $objOut } } } Write-Host "`tGetEnrollmentStatusPage{}" Function GetEnrollmentStatusPage { [cmdletbinding()] param ( [Parameter(mandatory=$false)] $Id, [Parameter(mandatory=$false)] $Filter ) # Defining Variables $version = "beta" $resource = "deviceManagement/deviceEnrollmentConfigurations" $uri = "https://graph.microsoft.com/$version/$resource" if ($PSBoundParameters.ContainsKey('Id')) { $uri += "/$id" } if ($PSBoundParameters.ContainsKey('Filter')) { $uri += "?`$Filter=$Filter" } try { Write-Verbose "Invoke Graph Request" $response = Invoke-MgGraphRequest -Uri $uri -Method Get if ($id) { $objOut = $response } else { $objOut = $response.Value | Where { $_.'@odata.type' -eq "#microsoft.graph.windows10EnrollmentCompletionPageConfiguration" } } } catch { throw $_ } Write-Output $objOut } Write-Host "`tNewEnrollmentStatusPage{}" Function NewEnrollmentStatusPage { <# .SYNOPSIS Creates a new Intune Enrollment Status Page (ESP) .DESCRIPTION This function creates an Autopilot Enrollment Status Page (ESP) for Intune Enrollments .PARAMETER DisplayName Type: String - Configure the display name of the enrollment status page .PARAMETER Description Type: String - Configure the description of the enrollment status page .PARAMETER ShowProgress Type: Switch - Configure the option: Show app and profile installation progress .PARAMETER AllowCollectLogs Type: Switch - Configure the option: Allow users to collect logs about installation errors .PARAMETER Message Type: String - Configure the option: Show custom message when an error occurs .PARAMETER AllowUseOnFailure Type: Switch - Configure the option: Allow users to use device if installation error occurs .PARAMETER AllowResetOnError Type: Switch - Configure the option: Allow users to reset device if installation error occurs .PARAMETER BlockDeviceUntilComplete Type: Switch - Configure the option: Block device use until all apps and profiles are installed .PARAMETER TimeoutInMinutes Type: Integer - Configure the option: Show error when installation takes longer than specified number of minutes .PARAMETER Replace Type: Switch - When using this parameter any ESP with the same name will be deleted and recreated .PARAMETER WhatIf Common parameter WhatIf is supported .PARAMETER Confirm Common parameter Confirm is supported .EXAMPLE $esp = NewEnrollmentStatusPage -DisplayName 'Autopilot ESP' -Description 'ESP for Autopilot devices' -ErrorMessage "An error occured, please contact your support" -ShowProgress -AllowResetOnError -Replace -verbose .EXAMPLE NewEnrollmentStatusPage -ErrorMessage "An error occured, please contact your support" -ShowProgress -AllowResetOnErrortrue .NOTES Version : 2024-07-04 Author : Frits van Drie (3-link.nl) Releases: initial release #> [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="Medium")] # 'none', 'Low', ('Medium'), 'High' param ( [Parameter(Mandatory=$true)] [string]$displayName, [Parameter(Mandatory=$false)][string]$Description, [Parameter(Mandatory=$false)][switch]$ShowProgress, [Parameter(Mandatory=$false)][switch]$AllowCollectLogs, [Parameter(Mandatory=$false)][switch]$BlockDeviceSetupRetryByUser, [Parameter(Mandatory=$false)][string]$ErrorMessage, [Parameter(Mandatory=$false)][switch]$AllowUseOnFailure, [Parameter(Mandatory=$false)][switch]$AllowResetOnError, [Parameter(Mandatory=$false)][switch]$BlockDeviceUntilComplete, [Parameter(Mandatory=$false)][int]$Priority = 100, [Parameter(Mandatory=$false)][validateRange(1,1440)][int]$TimeoutInMinutes = 90, [Parameter(Mandatory=$false)][switch]$Replace ) if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference') } $Confirm = ( $PSBoundParameters.'Confirm'.IsPresent -eq $true ) if (-not $Confirm) { $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference') } $WhatIf = ( $PSBoundParameters.'WhatIf'.IsPresent -eq $true ) if (-not $WhatIf) { $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference') } if ($PSBoundParameters.Keys -notcontains 'Description') { Write-Verbose "Using default description" $description = "The Enrollment Status Page appears during initial device setup and during first user sign in. If enabled, users can see the configuration progress of assigned apps and profiles targeted to their device" Write-Verbose "`tDescription: $Description" } $hashBody = @{ '@odata.type' = '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' deviceEnrollmentConfigurationType = "windows10EnrollmentCompletionPageConfiguration" displayName = $DisplayName description = $Description priority = $Priority roleScopeTagIds = @("0") showInstallationProgress = $($ShowProgress.IsPresent) installProgressTimeoutInMinutes = $TimeoutInMinutes customErrorMessage = $ErrorMessage allowLogCollectionOnInstallFailure = $($AllowCollectLogs.IsPresent) trackInstallProgressForAutopilotOnly = $false selectedMobileAppIds = @() allowDeviceResetOnInstallFailure = $($AllowResetOnError.IsPresent) allowDeviceUseOnInstallFailure = $($AllowUseOnFailure.IsPresent) installQualityUpdates = $false disableUserStatusTrackingAfterFirstUser = $false allowNonBlockingAppInstallation = $false blockDeviceSetupRetryByUser = $($BlockDeviceSetupRetryByUser.IsPresent) } If ( $PSBoundParameters.Keys -notContains 'ShowProgress') { Write-Verbose "Some parameter values might have changed because 'Show app and profile configuration progress' is disabled" Write-Verbose "`tblockDeviceSetupRetryByUser : $true" $hashBody.blockDeviceSetupRetryByUser = $true Write-Verbose "`tallowDeviceUseOnInstallFailure : $false" $hashBody.allowDeviceUseOnInstallFailure = $false Write-Verbose "`tallowDeviceResetOnInstallFailure : $false" $hashBody.allowDeviceResetOnInstallFailure = $false Write-Verbose "`tallowLogCollectionOnInstallFailure: $false" $hashBody.allowLogCollectionOnInstallFailure = $false Write-Verbose "`tcustomErrorMessage : ''" $hashBody.customErrorMessage = "" Write-Verbose "`tinstallProgressTimeoutInMinutes : 90" $hashBody.installProgressTimeoutInMinutes = 90 } elseif ($PSBoundParameters.ContainsKey('BlockDeviceSetupRetryByUser')) { Write-Verbose "Some parameter values might have changed because BlockDeviceSetupRetryByUser is enabled" Write-Verbose "`tallowDeviceUseOnInstallFailure : $false" $hashBody.allowDeviceUseOnInstallFailure = $false Write-Verbose "`tallowDeviceResetOnInstallFailure : $false" $hashBody.allowDeviceResetOnInstallFailure = $false } $version = "beta" $resource = "deviceManagement/deviceEnrollmentConfigurations" $uri = "https://graph.microsoft.com/$version/$resource" if ( $response = (GetEnrollmentStatusPage -Filter "displayName eq '$displayName'") ) { if ($PSBoundParameters.ContainsKey('Replace')) { foreach ($esp in $response) { Write-Verbose "Remove existing ESP: $($esp.displayName)" # ShouldProcess(message, target, action | target,action | target) if ( ($yesToAll) -or ($PSCmdlet.ShouldProcess("Intune ESP: $($esp.displayName) [$($esp.id)])", 'Remove ESP')) ) { try { $uriESP = "$uri/$($esp.id)" Write-Verbose "Invoke Graph Request" $objOut = Invoke-MgGraphRequest -Uri $uriESP -Method DELETE -ErrorAction Stop Write-Verbose "Successfully removed ESP: $($esp.id)" } catch { throw $_ } } elseif ( -not $yesToAll ) { Write-Verbose "Existing ESP is not removed because the WhatIf parameter was used" } } } else { throw "ESP with this name already exists: $displayName. Use the Replace parameter if you want to delete and recreate it" } } <# showInstallationProgress Show app and profile configuration progress installProgressTimeoutInMinutes Show an error when installation takes longer than specified number of minutes customErrorMessage Show custom message when time limit or error occurs allowLogCollectionOnInstallFailure Turn on log collection and diagnostics page for end users trackInstallProgressForAutopilotOnly Only show page to devices provisioned by out-of-box experience (OOBE) selectedMobileAppIds Block device use until all apps and profiles are installed allowDeviceResetOnInstallFailure Allow users to reset device if installation error occurs allowDeviceUseOnInstallFailure Allow users to use device if installation error occurs Block device use until required apps are installed if they are assigned to the user/device installQualityUpdates disableUserStatusTrackingAfterFirstUser allowNonBlockingAppInstallation blockDeviceSetupRetryByUser #> $jsonBody = $hashBody | ConvertTo-Json Write-Verbose "JSON payload:`n$jsonBody" # ShouldProcess(message, target, action | target,action | target) if ( ($yesToAll) -or ($PSCmdlet.ShouldProcess("DisplayName: $displayName", 'Create new ESP')) ) { try { Write-Verbose "Invoke Graph Request" $objOut = Invoke-MgGraphRequest -Uri $uri -Method POST -Body $jsonBody -ContentType "application/json" } catch { throw $_ } Write-Output $objOut } } Write-Host "`tGet-3gIntuneDeviceConfigurationPolicy{}" Function Get-3gIntuneDeviceConfigurationPolicy { <# .NOTES Name: Get-3gIntuneDeviceConfigurationPolicy Author: Frits van Drie (3-Link.nl) Date: 2024-06-18 #> [CmdletBinding()] param() $batch = @' { "requests":[ { "id":"/deviceManagement/configurationPolicies", "method":"GET", "url":"/deviceManagement/configurationPolicies?$top=1000&$select=id,name,lastModifiedDateTime,roleScopeTagIds,createdDateTime&$orderBy=name asc" }, { "id":"/deviceManagement/deviceConfigurations", "method":"GET", "url":"/deviceManagement/deviceConfigurations?$top=1000&$select=id,displayName,lastModifiedDateTime,roleScopeTagIds,createdDateTime&$orderBy=displayName asc" }, { "id":"/deviceManagement/groupPolicyConfigurations", "method":"GET", "url":"/deviceManagement/groupPolicyConfigurations?$top=1500" }, { "id":"/deviceAppManagement/mobileAppConfigurations", "method":"GET", "url":"/deviceAppManagement/mobileAppConfigurations?$top=1000&$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig eq true" }, { "id":"/deviceManagement/resourceAccessProfiles", "method":"GET", "url":"/deviceManagement/resourceAccessProfiles?$top=1000" } ] } '@ $uri = 'https://graph.microsoft.com/beta/$batch' Write-Verbose "Invoke-MgGraphRequest" Write-Verbose "`tUri : '$uri'" Write-Verbose "`tMethod: POST" $restResult = (Invoke-MgGraphRequest -Method POST -Uri $uri -Body $batch -OutputType PSObject -ErrorAction Stop) #.value foreach ($response in $restResult.responses) { foreach ($item in $response.body.value) { $ht = @{ '@odata.type' = $item.'@odata.type' id = $item.id createdDateTime = $item.createdDateTime lastModifiedDateTime = $item.lastModifiedDateTime roleScopeTagIds = $item.roleScopeTagIds resourceType = $response.id } if ($item.displayName) { $ht.Add('displayName', $item.displayName) } elseif ($item.profileName) { $ht.Add('displayName', $item.profileName) } elseif ($item.name) { $ht.Add('displayName', $item.name) } else { $ht.Add('displayName', '') } $objOut = [PSCustomObject]$ht Write-Verbose ("Output: {0} [{1}] " -f ($objOut.displayName).PadRight(60), $objOut.id) Write-Output $objOut } } } Write-Host "`tRemove-3gIntuneDeviceConfigurationPolicy{}" Function Remove-3gIntuneDeviceConfigurationPolicy { [CmdletBinding()] param( [Parameter(mandatory=$true)] [String[]]$Id, [Parameter(mandatory=$true)] [ValidateSet( '/deviceManagement/configurationPolicies', '/deviceManagement/deviceConfigurationProfiles', '/deviceManagement/deviceConfigurations', '/deviceManagement/groupPolicyConfigurations', '/deviceAppManagement/mobileAppConfigurations', '/deviceAppManagement/mobileAppConfigurations') ] [String[]]$ResourceType, [string]$ApiVersion = 'beta' ) $uri = "https://graph.microsoft.com/$ApiVersion$ResourceType/$Id" Write-Verbose "Invoke-MgGraphRequest" Write-Verbose "`tUri : '$uri'" Write-Verbose "`tMethod: DELETE" try { $restResult = (Invoke-MgGraphRequest -Method DELETE -Uri $uri -OutputType PSObject -ErrorAction Stop) #.value Write-Verbose 'Success' } catch { Write-Verbose 'failed' throw $_ } Write-Output $restResult } Write-Host "`tNewMgDeviceManagementDeviceConfigurationAssignment{}" Function NewMgDeviceManagementDeviceConfigurationAssignment { [CmdletBinding()] param ( [parameter(Mandatory=$true)] $deviceConfigurationId, [parameter(Mandatory=$true)] $target ) Write-Host "`t`tAssigning group`t" -NoNewline try { $assignments = New-MgBetaDeviceManagementDeviceConfigurationAssignment -DeviceConfigurationId $deviceConfigurationId -Target $target -ErrorAction Stop Write-Host "Success" -f Green } catch { Write-Host "Red" -f Red } Write-Host "`tCurrent Assignments:" $assignments = Get-MgBetaDeviceManagementDeviceConfigurationAssignment -DeviceConfigurationId $deviceConfigurationId foreach ($item in $assignments.target.additionalProperties) { $odataType = $item.'@odata.type' if ($odataType -like '*exclusionGroupAssignmentTarget') { Write-Host "`t`tExcluded: " -f Yellow -NoNewline } else { Write-Host "`t`tIncluded: " -f Green -NoNewline } if ($item.groupId) { $grpDisplayName = (Get-MgGroup -GroupId $item.groupId).DisplayName Write-Host $grpDisplayName "[$($item.groupId)]" } else { Write-Host $item.'@odata.type' } } } Write-Host "`tNew3gDeviceManagementDeviceConfigurationAssignment{}" Function New3gDeviceManagementDeviceConfigurationAssignment { [CmdletBinding()] param ( [parameter(Mandatory=$true)] $deviceConfigurationId, [parameter(Mandatory=$true)] $target ) $jsonTarget = $target | ConvertTo-Json $jsonBody = @" { "target": $jsonTarget } "@ Write-Host "`t`tAssigning group`t" -NoNewline $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations/$deviceConfigurationId/assignments" try { $assignments = (Invoke-MgGraphRequest -Method POST -Uri $uri -Body $jsonBody -ErrorAction Stop).value Write-Host 'success' -f Green } catch { Write-Host 'failed' -f Red throw $_ } Write-Host "`tGet Current Assignments`t" -NoNewline $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations/$deviceConfigurationId/assignments" try { $assignments = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).value Write-Host 'success' -f Green } catch { Write-Host 'failed' -f Red throw $_ } foreach ($item in $assignments.target) { $odataType = $item.'@odata.type' if ($odataType -like '*exclusionGroupAssignmentTarget') { Write-Host "`t`tExcluded: " -f Yellow -NoNewline } else { Write-Host "`t`tIncluded: " -f Green -NoNewline } if ($item.groupId) { #$grpDisplayName = (Get-MgGroup -GroupId $item.groupId).DisplayName $uri = "https://graph.microsoft.com/beta/groups/$($item.groupId)" $grpDisplayName = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).displayName Write-Host $grpDisplayName "[$($item.groupId)]" } else { Write-Host $item.'@odata.type' } } } Write-Host "`tSetServiceAccount{}" Function SetServiceAccount { <# .NOTES Author : Frits van Drie( 3-link.nl) Releases: 2024-07-07 Initial release #> [CmdLetBinding(DefaultParameterSetName="SvcAccount")] PARAM ( [Parameter(ParameterSetName='SvcAccount')] [Parameter(ParameterSetName='SystemAccount')] [Parameter(Mandatory=$true, Position=0)] [String] $serviceName, [Parameter(ParameterSetName='SvcAccount')] [Parameter(ParameterSetName='SystemAccount')] [Parameter(Mandatory=$false, Position=1)] [String] $computerName = 'localhost', [Parameter(ParameterSetName='SvcAccount')] [Parameter(ParameterSetName='SystemAccount')] [Parameter(Mandatory=$false, Position=3)] [ValidateSet('WMI', 'DCOM', 'WSMAN')] [String] $connectionMethod = 'WSMAN', [Parameter(ParameterSetName='SvcAccount')] [Parameter(Mandatory=$false, Position=4)] [ValidateNotNullOrEmpty()] [String] $serviceAccountName, [Parameter(ParameterSetName='SystemAccount')] [Parameter(Mandatory=$false, Position=4)] [validateset('LocalSystem', 'NT AUTHORITY\System', 'NT AUTHORITY\LocalService', 'NT AUTHORITY\NetworkService')] [String] $SystemAccountName='LocalSystem', [Parameter(ParameterSetName='SvcAccount')] [Parameter(Mandatory=$false, Position=5)] [ValidateNotNullOrEmpty()] [String]$serviceAccountPassword, [Parameter(ParameterSetName='SvcAccount')] [Parameter(Mandatory=$false, Position=6)] [ValidateNotNullOrEmpty()] [String] $RunAsUserName, [Parameter(ParameterSetName='SvcAccount')] [Parameter(Mandatory=$false, Position=7)] [ValidateNotNullOrEmpty()] [System.Security.SecureString] $RunAsPassword ) if (($serviceAccountName) -and !($serviceAccountPassword)) { Write-Verbose "No password specified for RunAsUser $RunAsUserName" return $false } if ( ($PSCmdlet.ParameterSetName -eq 'SvcAccount') -and !($serviceAccountPassword)) { Write-Verbose "No password specified for ServiceAccount $serviceAccountName" return $false } Write-Verbose "New ServiceAccountName and Password" if ($PSCmdlet.ParameterSetName -eq 'SystemAccount') { $serviceAccountName = $SystemAccountName $serviceAccountPassword = "notneeded" #$serviceAccountPassword = ("notneeded" | ConvertTo-SecureString -AsPlainText -Force) } # Check input: Username <# user OK domain\user OK pc\user OK \user OK .\user ERROR localhost\user ERROR notexistinguser ERROR Check after connecting #> if ($serviceAccountName.split('\')[0] -eq '.') { $serviceAccountName = $serviceAccountName.Replace('.',$computerName) } $serviceAccountName = $serviceAccountName.Replace('localhost',$env:COMPUTERNAME) $Connected = $false # Test if Computer exist if ($computerName -notin 'localhost', $env:COMPUTERNAME ) { try { Resolve-DnsName $computerName -ErrorAction Stop |Out-Null Write-Verbose “[$computerName] Computername found in DNS" } catch { Write-Verbose “[$computerName] ERROR: Computername not found in DNS" Return $false } } # Add Credentials $Filter = "name='$serviceName'" $Parms = @{ 'ComputerName' = $computerName; "Filter" = $Filter } if (($computerName -notin 'localhost', $env:COMPUTERNAME ) -and ($RunAsUserName) -and ($RunAsUserPassword)) { $CredRunAs = New-Object System.Management.Automation.PSCredential ($RunAsUserName, $RunAsPassword) $Parms.add("Credential",$CredRunAs) } If ($connectionMethod -eq "WMI") { try { Write-Verbose "[$computerName`:$odjSvcName] - Retrieving information using $connectionMethod" $svc = $null $svc = Get-WmiObject win32_service @Parms -ErrorAction Stop Write-Verbose "[$computerName`:$odjSvcName] - Previous username: $($svc.StartName)" Write-Verbose "[$computerName`:$odjSvcName] - Previous state : $($svc.State)" } catch { Write-Verbose "[$computerName`:$odjSvcName] - ERROR connecting using $connectionMethod" # $connectionMethod = "DCOM" } if ($svc) { <# https://4sysops.com/archives/managing-services-the-powershell-way-part-8-service-accounts/ $svc = get-wmiobject win32_service -filter "name='BITS'" $svc.GetMethodParameters("change") 1 System.String DisplayName, 2 System.String PathName, 3 System.Byte ServiceType, 4 System.Byte ErrorControl, 5 System.String StartMode, 6 System.Boolean DesktopInteract, 7 System.String StartName, 8 System.String StartPassword, 9 System.String LoadOrderGroup, 10 System.String[] LoadOrderGroupDependencies, 11 System.String[] ServiceDependencies #> $return = $svc.Change($null,$null,$null,$null,$null,$null,$serviceAccountName,$serviceAccountPassword) if ($return.ReturnValue -eq 0) { $Connected = $true Write-Verbose "[$computerName`:$odjSvcName] - New username: $serviceAccountName" } else { Write-Verbose "[$computerName`:$odjSvcName] - ERROR modifying service (ReturnValue: $return)" Write-Verbose "[$computerName`:$odjSvcName] - Check if account ($serviceAccountName) exists" } } } If ($connectionMethod -in “WSMAN”,"DCOM") { try { Write-Verbose "[$computerName] Connecting using $connectionMethod" $opt = New-CimSessionOption –Protocol $connectionMethod $sess = New-CimSession –ComputerName $computerName –SessionOption $opt -ErrorAction Stop Write-Verbose "[$computerName] Connected using $connectionMethod" $Connected = $true $Parms.Remove(“ComputerName”) $Parms.add("CimSession",$sess) $svc = $null # $Parms $svc = Get-CimInstance win32_service @Parms -ErrorAction Stop $ret = Invoke-CimMethod ` -CimSession $sess ` -Query "SELECT * FROM Win32_Service WHERE Name=`'$odjSvcName`'" ` -MethodName Change ` -Arguments @{'StartName' =$serviceAccountName; 'StartPassword'=$serviceAccountPassword } if ($ret.ReturnValue -eq 0){ Write-Verbose “[$computerName`:$serviceName] - Service successfully modified" } else { Write-Verbose "[$computerName`:$serviceName] - ERROR modifying service" Write-Verbose "[$computerName`:$serviceName] - Check if account ($serviceAccountName) exists" } } catch { Write-Verbose "[$computerName] Connection ERROR using $connectionMethod" $connectionMethod = "WSMAN" Write-Verbose "[$computerName] Retrying using $connectionMethod" #return $false } } If ($connectionMethod -eq "xxx") { $InvokeCommandParms = @{ 'ComputerName' = $computerName; } if ($CredRunAs) { $InvokeCommandParms.Add('Credential',$CredRunAs) } try { Write-Verbose "[$computerName] Connecting using $connectionMethod" Invoke-Command @InvokeCommandParms { Write-Verbose "[$env:ComputerName] Connected using $Using:ConnectionMethod" $Parms = $Using:Parms $Parms.Remove("Credential") $svc = Get-WmiObject win32_service @Parms -ErrorAction Stop } $Connected = $true } catch { Write-Verbose "[$computerName] ERROR connecting using $connectionMethod" throw $_ } } # restart service if ($svc.State -eq 'Stopped') { Write-Verbose "[$computerName`:$serviceName] - Service will not be started because it was not running" } elseif ($return.ReturnValue -eq 0) { $svc.StopService() | Out-Null while ($svc.Started){ Write-Verbose "[$computerName`:$serviceName] - Stopping service" sleep 1 $svc = Get-WmiObject win32_service @Parms } Write-Verbose "[$computerName`:$serviceName] - service is $($svc.State)" # start Service try { Write-Verbose "[$computerName`:$serviceName] - Starting service" $return = ($svc.StartService()).ReturnValue if ($return -ne 0) { throw } } catch { Write-Verbose "[$computerName`:$serviceName] - ERROR: cannot start service (ReturnValue: $return)" Write-Verbose "[$computerName`:$serviceName] - ERROR: Check if user account has 'log on as a service' rights" } } return $true } #endregion #region: Connections Write-Host "Setup: Connections" -f Cyan Write-Host "`tMicrosoft Graph" $scopes = @( 'Device.ReadWrite.All', 'DeviceManagementManagedDevices.ReadWrite.All', 'DeviceManagementApps.ReadWrite.All', 'DeviceManagementConfiguration.ReadWrite.All', 'DeviceManagementServiceConfig.ReadWrite.All', 'Group.ReadWrite.All', 'Organization.ReadWrite.All', 'Policy.ReadWrite.MobilityManagement', 'User.ReadWrite.All' ) try { Write-Host "`t`tscopes: $($scopes -join "`n`t ")" Write-Host "`t`tState :`t" -NoNewline Connect-MgGraph -Scopes $scopes -NoWelcome -ErrorAction Stop Write-Host "Connected" -f Green $tenantId = (Get-MgContext).tenantId } catch { Write-Host "failed" -f Red Write-Error $_ } if ($HybridJoin) { Write-Host "`tDomain Controller (WinRM): $($domainControllerName.split('.')[0])`t" -NoNewline $sessionDC = Get-PSSession -ComputerName $domainControllerName -ErrorAction SilentlyContinue $retry = 0 while ($sessionDC.State -ne 'Opened' -and $retry -le 10) { try { $retry++ Write-Host "." -NoNewLine $sessionDC = New-PSSession -ComputerName $domainControllerName -ErrorAction Stop } catch { if ($retry -gt 10) { Write-Host ' failed' -f Red throw "Failed to connect to $domainControllerName using WinRM" } } } Write-Host " connected" -f Green Write-Host "`tODJ Connector (WinRM): $odjConnectorServer " -NoNewline $sessionSVR = Get-PSSession -ComputerName $odjConnectorServer -ErrorAction SilentlyContinue $retry = 0 while ($sessionSVR.State -ne 'Opened' -and $retry -le 10) { try { $retry++ Write-Host "." -NoNewLine $sessionSVR = New-PSSession -ComputerName $odjConnectorServer -ErrorAction Stop } catch { if ($retry -gt 10) { Write-Host ' failed' -f Red throw "Failed to connect to $odjConnectorServer using WinRM" } } } Write-Host " connected" -f Green } #endregion #region: [Entra ] Assigned User if ($assignUser) { Write-Host "Entra User account" -f Cyan write-Host "`tGet User: $assignedUserDisplayName`t" -NoNewline try { $uri = "https://graph.microsoft.com/beta/users?`$filter=displayName eq '$assignedUserDisplayName'" if ($assignedUser = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).value) { Write-Host "found" -f Green } else { Write-Host 'not found' -f Red } } catch { Write-Host "failed" -f Red throw $_ } if ( -not $assignedUser) { $jsonBody = @" { "accountEnabled" : true, "givenName" : "$assignedUserGivenName", "surname" : "$assignedUserSurName", "displayName" : "$assignedUserDisplayName", "userPrincipalName": "$assignedUserUpn", "mailNickname" : "$($assignedUserDisplayName.replace(' ',''))", "passwordProfile" : { "forceChangePasswordNextSignIn": false, "password" : "$assignedUserPassPlain" }, "ageGroup" : "Adult", "department" : "Learning", "usageLocation" : "NL" } "@ Write-Host "`tCreate user: $assignedUserDisplayName`t" -NoNewline try { $uri = 'https://graph.microsoft.com/beta/users' $assignedUser = Invoke-MgGraphRequest -Method POST -Uri $uri -Body $jsonBody -ErrorAction Stop write-Host "[$($assignedUser.id)]`tsuccess" -f Green } catch { Write-Host "failed" -f Red throw $_ } } } #endregion #region: Prerequisites Write-Host "Setup: Prerequisites" -f Cyan #region: Entra Connect if ( $HybridJoin ) { Write-Host "`tEntra Connect" Write-Host "`t`tSCP configured: " -NoNewline try { Invoke-Command -Session $sessionDC -ErrorAction Stop { $scp = "CN=62a0ff2e-97b9-4513-943f-0d221bd30080,CN=Device Registration Configuration,CN=Services,CN=Configuration,dc=sure,dc=local" try { if ($objSCP = Get-ADObject $scp -Properties Keywords -ErrorAction Stop) { Write-Host 'True' -f Green Write-Host "`t`tTenant name : $(($objSCP.Keywords | where {$_ -match 'azureADName'}).split(':')[-1])" } else { throw "SCP not configured" } } catch { Write-Host 'False' -f Red throw $_ } } } catch { throw $_ } Write-Host "`t`tLDAP Path OU : " -NoNewline if ( $SyncedOU ) { Write-Host $SyncedOU } else { try { $SyncedOU = Invoke-Command -Session $sessionSVR { $module = "$env:ProgramFiles\Microsoft Azure AD Sync\Bin\ADSync\ADSync" Import-Module $module -ErrorAction Stop # Connect information for your on-premises domain. $syncConnector = Get-ADSyncConnector -ErrorAction Stop | Where-Object {$_.Name -notmatch ' - aad'} # OU inclusion list $syncOUIncluded = ($syncConnector.Partitions.ConnectorPartitionScope.ContainerInclusionList)[0] Write-Output $syncOUIncluded # OU exclusion list #$syncOUExcluded = $syncConnector.Partitions.ConnectorPartitionScope.ContainerExclusionList } Write-Host "$SyncedOU" -f Green } catch { Write-Host 'failed' -f Red throw $_ } } } #endregion #region: Server OS is minimal WS2016 (buildnr: 14393) if ($HybridJoin) { Write-Host "`tServer OS" Write-Host "`t`tMinimal version: WS 2016" $osVersion = Invoke-Command -Session $sessionSVR -ErrorAction Stop -ScriptBlock { [Environment]::OSVersion.Version } if ($osVersion.Build -ge 14393) { Write-Host "`t`tCurrent version: $($osVersion.Build)" -f Green } else { Write-Host "`t`tCurrent version: $($osVersion.Build)" -f Red throw "OS Version ($($osVersion.Build)) on $odjConnectorServer is not supported. Upgrade your server OS" } } #endregion #region: DNS Names Write-Host "`tDNS domains" $result = @() $DnsNameList = @( "EnterpriseRegistration.windows.net", "login.MicrosoftOnline.com", "device.login.MicrosoftOnline.com", "autologon.MicrosoftAzureAD-sso.com" # for Seamless SSO ) foreach($DnsName in $DnsNameList) { try { Write-Host "`t`t$DnsName`t" -NoNewline $null = Resolve-DnsName $DnsName -ErrorAction Stop Write-Host "success" -f Green $result += [PSCustomObject]@{'DNSName' = $DnsName ;'Resolved' = 'True'} } catch { $result += [PSCustomObject]@{'DNSName' = $DnsName ;'Resolved' = 'False'} Write-Host "failed" -f Red } } #endregion #region: Assigned user exists Write-Host "`tAssigned user" if ($assignUser) { Write-Host "`t`tUser: $assignedUserUpn`t" -NoNewLine try { $uri = "https://graph.microsoft.com/beta/users/$assignedUserUpn" $assignedUser = Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop Write-Host 'found' -f Green } catch { Write-Host 'not found' -f Red throw $_ } Write-Host "`t`tUsage Location: " -NoNewLine if ($assignedUser.UsageLocation) { Write-Host "$($assignedUser.UsageLocation)" -f Yellow } else { Write-Host "missing" -f Red $usageLocation = 'US' Write-Host "`tSet Usage Location: $usageLocation`t" -NoNewline try { $jsonBody = "{usageLocation: `"$usageLocation`"}" $uri = "https://graph.microsoft.com/beta/users/$assignedUserUpn" Invoke-MgGraphRequest -Method PATCH -Uri $uri -Body $jsonBody -ErrorAction Stop Write-Host 'success' -f Green } catch { Write-Host 'failed' -f Red throw $_ } } } #endregion #region: Licenses Write-Host "`tLicenses:" $requiredSubscriptions = 'EMSPREMIUM' #$subscribedSku = Get-MgSubscribedSku -ErrorAction Stop Write-Host "`t`tGet all licensed products`t" -NoNewline $uri = "https://graph.microsoft.com/beta/subscribedSkus" try { $subscribedSku = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).value Write-Host "found $($subscribedSku.count)" -f Green } catch { Write-Host 'failed' -f Red throw $_ } if ($assignUser) { Write-Host "`t`tGet user licenses" foreach ($userName in $assignedUser.displayName) { Write-Host "`t`t`tUser: $userName`t" -NoNewline #$userLicenses = Get-MgUserLicenseDetail -UserId $assignedUser.id -ErrorAction Stop $uri = "https://graph.microsoft.com/beta/users/$($assignedUser.id)/licenseDetails" try { $userLicenses = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).value Write-Host "found $($userLicenses.count)" -f Green } catch { Write-Host 'failed' -f Red throw $_ } foreach ($product in $requiredSubscriptions) { Write-Host "`t`t`tSku : $product`t" -NoNewline if ($product -in $userLicenses.skuPartNumber) { Write-Host 'licensed' -f Green continue } Write-Host 'missing license' -f Red $sku = $subscribedSku | Where SkuPartNumber -eq $product if ( -not $sku) { Write-Host "`t`t`tProduct not found" -f Red throw "No subscription found for product: $product" } Write-Host "`t`tAssign license`t" -NoNewline $params = @{ addLicenses = @( @{ disabledPlans = @() skuId = $sku.skuId } ) removeLicenses = @() } $jsonBody = @" { "addLicenses": [ { "disabledPlans": [], "skuId" : "$($sku.skuId)" } ], "removeLicenses": [] } "@ try { #$userLicense = Set-MgUserLicense -UserId $assignedUser.Id -BodyParameter $params -ErrorAction Stop $uri = "https://graph.microsoft.com/beta/users/$($assignedUser.id)/assignLicense" $userLicense = (Invoke-MgGraphRequest -Method POST -Uri $uri -Body $jsonBody -ContentType 'application/json' -ErrorAction Stop).value Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } } # foreach product } # foreach username } #endregion # Entra Connect ID must be configured for Hybrid Join # https://docs.microsoft.com/en-us/windows/deployment/windows-autopilot/windows-autopilot-requirements#networking-requirements #endregion #region: Overview Write-Host "Overview" -f Cyan Write-Host "`tScript : $PSCommandPath" #$($myInvocation.InvocationName)" Write-Host "`tTenant Name : $tenantDnsName" Write-Host "`tTenant Id : $tenantId" Write-Host "`tHybrid Join : $HybridJoin" if ($HybridJoin) { Write-Host "`tAD Domain : $domainDnsName" Write-Host "`tDC : $domainControllerName (WinRM: $($sessionDC.State))" Write-Host "`tSynced OU : $SyncedOU" Write-Host "`tUsers OU : $ldapPathUsers" Write-Host "`tODJ Server : $odjConnectorServer (WinRM: $($sessionSVR.State))" Write-Host "`tSvc Account : $odjSvcAccountName" Write-Host "`tDevice group : $autopilotGroupName" } Write-Host "`tLicensed group: $mdmUsersGroupName" Write-Host "`tAssigned User : " -NoNewline if ($assignUser) { Write-Host "$assignedUserUpn [Pass: $assignedUserPassPlain]" } else { Write-Host } Write-Host "`tEnrollPolicy : $enrollmentProfileName" Write-Host "`tESP : $espDisplayName" Write-Host "`tMgGraph : $(if (Get-MgContext) {'Connected'} else {'Not connected'})" #endregion ################################################## #region: [Entra ] Intune Users group Write-Host "Entra Intune Users group" -f Cyan write-Host "`tGet existing Entra group: $mdmUsersGroupName`t" -NoNewline try { #[array]$entraGroups = Get-MgGroup -Filter "displayName eq '$autopilotGroupName'" -ErrorAction Stop $uri = "https://graph.microsoft.com/beta/groups?`$filter=displayName eq '$mdmUsersGroupName'" [array]$entraGroups = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).value Write-Host "found $($entraGroups.count)" -f Yellow } catch { Write-Host "failed" -f Red throw $_ } foreach ($group in $entraGroups) { write-Host "`tRemove group: $mdmUsersGroupName`t" -NoNewline try { $uri = "https://graph.microsoft.com/beta/groups/$($group.id)" $return = Invoke-MgGraphRequest -Method DELETE -Uri $uri -ErrorAction Stop Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } } Write-Host "`tCreate group: $mdmUsersGroupName`t" -NoNewline try { #$autopilotGroup = New-MgGroup -DisplayName $autopilotGroupName -MailEnabled:$false -MailNickName $autopilotGroupName.Replace(' ','') -GroupTypes 'DynamicMembership' -MembershipRule $apMembershipRule -MembershipRuleProcessingState 'On' -SecurityEnabled -ErrorAction Stop $jsonBody = @" { "DisplayName" : "$mdmUsersGroupName", "MailEnabled" : false, "MailNickName" : "$($mdmUsersGroupName.Replace(' ',''))", "GroupTypes" : [ "DynamicMembership" ], "MembershipRule" : "$($mdmMembershipRule.Replace('"','\"'))", "MembershipRuleProcessingState": "On", "SecurityEnabled": true } "@ #| ConvertFrom-Json $uri = 'https://graph.microsoft.com/beta/groups' $mdmGroup = Invoke-MgGraphRequest -Method POST -Uri $uri -Body $jsonBody -ErrorAction Stop write-Host "[$($mdmGroup.id)]`tsuccess" -f Green } catch { Write-Host "failed" -f Red throw $_ } if ($assignUser) { Write-Host "`tProcessing membershiprule for assigned user " -NoNewline try { do { Write-Host '.' -NoNewline sleep 5 $uri = "https://graph.microsoft.com/beta/groups/$($mdmGroup.id)/members" [array]$mdmGroupMembers = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).value } until ($assignedUserUpn -in $mdmGroupMembers.UserPrincipalName) Write-Host " $assignedUserUpn" -f Green } catch { Write-Host 'failed' -f Red throw $_ } } #endregion #region: [Entra ] Autopilot group Write-Host "Entra Autopilot group" -f Cyan write-Host "`tGet existing Entra group: $autopilotGroupName`t" -NoNewline try { #[array]$entraGroups = Get-MgGroup -Filter "displayName eq '$autopilotGroupName'" -ErrorAction Stop $uri = "https://graph.microsoft.com/beta/groups?`$filter=displayName eq '$autopilotGroupName'" [array]$entraGroups = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).value Write-Host "found $($entraGroups.count)" -f Yellow } catch { Write-Host "failed" -f Red throw $_ } foreach ($group in $entraGroups) { write-Host "`tRemove group: $autopilotGroupName`t" -NoNewline try { $uri = "https://graph.microsoft.com/beta/groups/$($group.id)" $return = Invoke-MgGraphRequest -Method DELETE -Uri $uri -ErrorAction Stop Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } } Write-Host "`tCreate group: $autopilotGroupName`t" -NoNewline try { #$autopilotGroup = New-MgGroup -DisplayName $autopilotGroupName -MailEnabled:$false -MailNickName $autopilotGroupName.Replace(' ','') -GroupTypes 'DynamicMembership' -MembershipRule $apMembershipRule -MembershipRuleProcessingState 'On' -SecurityEnabled -ErrorAction Stop $jsonBody = @" { "DisplayName" : "$autopilotGroupName", "MailEnabled" : false, "MailNickName" : "$($autopilotGroupName.Replace(' ',''))", "GroupTypes" : [ "DynamicMembership" ], "MembershipRule" : "$($apMembershipRule.Replace('"','\"'))", "MembershipRuleProcessingState": "On", "SecurityEnabled": true } "@ #| ConvertFrom-Json $uri = 'https://graph.microsoft.com/beta/groups' $autopilotGroup = Invoke-MgGraphRequest -Method POST -Uri $uri -Body $jsonBody -ErrorAction Stop write-Host "[$($autopilotGroup.id)]`tsuccess" -f Green } catch { Write-Host "failed" -f Red throw $_ } #endregion #region: [Intune] Autopilot Enrollment profile Write-Host "Intune Autopilot Enrollment profile" -f Cyan Write-Host "`tGet existing profile(s): $enrollmentProfileName`t" -NoNewline try { #[array]$profiles = Get-MgBetaDeviceManagementWindowsAutopilotDeploymentProfile -Filter "displayName eq '$enrollmentProfileName'" -ErrorAction Stop -ExpandProperty * $uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles?`$filter=displayName eq '$enrollmentProfileName'" [array]$profiles = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).value Write-Host "found $($profiles.count)" -f Yellow } catch { Write-Host "failed" -f Red throw $_ } # Remove Profiles + Assignments foreach ($profile in $profiles) { try { Write-Host "`tRemove assignments from profile: $($profile.displayName)" #$assignments = Get-MgBetaDeviceManagementWindowsAutopilotDeploymentProfileAssignment -WindowsAutopilotDeploymentProfileId $profile.Id $uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($profile.id)/assignments" $assignments = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).value foreach ($assignmentId in $assignments.Id) { write-Host "`t`tID: $assignmentId`t" -NoNewline #Remove-MgBetaDeviceManagementWindowsAutopilotDeploymentProfileAssignment -WindowsAutopilotDeploymentProfileId $profile.Id -WindowsAutopilotDeploymentProfileAssignmentId $assignmentId -ErrorAction Stop $uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($profile.id)/assignments/$assignmentId" Invoke-MgGraphRequest -Method DELETE -Uri $uri -ErrorAction Stop Write-Host "success" -f Green } } catch { Write-Host "failed" -f Red throw $_ } try { Write-Host "`tRemoving profile: $($profile.displayName)`t" -NoNewline #Remove-MgBetaDeviceManagementWindowsAutopilotDeploymentProfile -WindowsAutopilotDeploymentProfileId $profile.Id $uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($profile.id)" Invoke-MgGraphRequest -Method DELETE -Uri $uri -ErrorAction Stop Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } } Write-Host "`tCreate new profile: $enrollmentProfileName`t" -NoNewline try { $scopes = 'DeviceManagementServiceConfig.ReadWrite.All' Connect-MgGraph -Scopes $scopes -NoWelcome $description = "Demo AutoPilot Profile" $jsonBody = @{ "@odata.type" = "$enrollmentType" displayName = "$($enrollmentProfileName)" description = "$($description)" language = 'os-default' extractHardwareHash = $false enableWhiteGlove = $false outOfBoxExperienceSettings = @{ "@odata.type" = "microsoft.graph.outOfBoxExperienceSettings" hidePrivacySettings = $true hideEULA = $true userType = 'standard' deviceUsageType = 'singleuser' skipKeyboardSelectionPage = $true hideEscapeLink = $true } enrollmentStatusScreenSettings = @{ '@odata.type' = "microsoft.graph.windowsEnrollmentStatusScreenSettings" hideInstallationProgress = $false allowDeviceUseBeforeProfileAndAppInstallComplete = $true blockDeviceSetupRetryByUser = $false allowLogCollectionOnInstallFailure = $true customErrorMessage = "An error has occured. Please contact your IT Administrator" installProgressTimeoutInMinutes = "90" allowDeviceUseOnInstallFailure = $true } } | ConvertTo-Json $uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles" $created_Profile = Invoke-MgGraphRequest -Uri $uri -Method POST -Body $jsonBody -ContentType 'application/json' $profileID = $created_Profile.ID # New-MgBetaDeviceManagementWindowsAutopilotDeploymentProfile Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } write-Host "`tAssign group: $($autopilotGroup.DisplayName)`t" -NoNewline try { $jsonBody = @" { "target": { "@odata.type":"#microsoft.graph.groupAssignmentTarget", "groupId":"$($autopilotGroup.Id)" } } "@ $uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($profileID)/assignments" $assignment = Invoke-MgGraphRequest -Uri $uri -Method POST -Body $jsonBody -ContentType 'application/json' Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } #endregion #region: [Intune] ESP Write-Host "Intune Enrollment Status Page (ESP)" -f Cyan Write-Host "`tGet ESP`t" -NoNewLine try { $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations" [array]$response = (GetEnrollmentStatusPage -Filter "displayName eq '$espDisplayName'") Write-Host "found $($response.count)" -f Yellow } catch { Write-Host "failed" -f Red } foreach ($esp in $response) { Write-Host "`tRemove ESP: $($esp.displayName)`t" -NoNewline try { $uriESP = "$uri/$($esp.id)" $objOut = Invoke-MgGraphRequest -Uri $uriESP -Method DELETE -ErrorAction Stop Write-Host "success" -f Green } catch { Write-Host "failed" -f Red } } Write-Host "`tCreate ESP" Write-Host "`t`tType: $autopilotType" Write-Host "`t`tName: $espDisplayName`t" -NoNewline try { $esp = NewEnrollmentStatusPage ` -displayName $espDisplayName ` -description 'ESP for demo Autopilot' ` -ShowProgress ` -allowCollectLogs ` -blockDeviceSetupRetryByUser:$true ` -ErrorMessage "Oops, an error occurred! Please contact your IT department" ` -allowUseOnFailure:$false ` -allowResetOnError ` -BlockDeviceUntilComplete ` -Priority 99 ` -timeoutInMinutes 90 ` -ErrorAction Stop ` -Replace Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } write-Host "`tAssign group: $($autopilotGroup.DisplayName)`t" -NoNewline try { $assignmentBody = @" { enrollmentConfigurationAssignments: [ { "target": { "@odata.type": "#microsoft.graph.groupAssignmentTarget", "groupId": "$($autopilotGroup.Id)" } } ] } "@ $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations/$($esp.id)/assign" $assignment = Invoke-MgGraphRequest -Uri $uri -Method POST -Body $assignmentBody -ContentType 'application/json' Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } #endregion #region: [Intune] Device Policy: Disable User ESP Write-Host "Device Policy: Disable User ESP" -f Cyan $omaName = 'SkipUserStatusPage' $omaUri = "./Vendor/MSFT/DMClient/Provider/MS DM Server/FirstSyncStatus/SkipUserStatusPage" $omaValue = $true Write-Host "`tCreate Device Configuration Policy" Write-Host "`t`tPolicy name: $devicePolicyESP" Write-Host "`t`tGet existing Policy`t" -NoNewline # $deviceConfigurations = Get-MgDeviceManagementDeviceConfiguration | Where DisplayName -eq $devicePolicyESP # Not retrieving all kinds of policies $deviceConfigurations = Get-3gIntuneDeviceConfigurationProfile | Where DisplayName -eq $devicePolicyESP Write-Host "found $(($deviceConfigurations|Measure-Object).count)" -f Green foreach ($config in $deviceConfigurations) { Write-Host "`t`tRemove existing Policy: $($config.id)`t" -NoNewline try { # Remove-MgDeviceManagementDeviceConfiguration -DeviceConfigurationId $config.Id -ErrorAction Stop $removed = Remove-3gIntuneDeviceConfigurationPolicy -Id $config.Id -ResourceType $config.resourceType -ErrorAction Stop Write-Host "Success" -f Green } catch { Write-Host "failed" -f Red throw $_ } } $jsonBody = @" { "@odata.type" : "#microsoft.graph.windows10CustomConfiguration", "displayName" : "$devicePolicyESP", "omaSettings" : [ { "@odata.type" : "#microsoft.graph.omaSettingBoolean", "displayName" : "$omaName", "omaUri" : "$omaUri", "value" : "$omaValue" } ] } "@ $test = $jsonBody | ConvertFrom-Json -ErrorAction Stop Write-Host "`t`tCreate new Policy`t" -NoNewline try { #New-MgDeviceManagementDeviceConfiguration -DisplayName $policyDisplayName -AdditionalProperties $jsonBody $method = 'POST' $version = 'beta' $uri = "https://graph.microsoft.com/$version/deviceManagement/deviceConfigurations" $restResult = (Invoke-MgGraphRequest -Method $method -Uri $uri -Body $jsonBody -ErrorAction Stop).value Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } # Assign policy $deviceConfigurations = Get-3gIntuneDeviceConfigurationProfile | Where DisplayName -eq $devicePolicyESP Write-Host "`t`tGet Group: $autopilotGroupName`t" -NoNewline $uri = "https://graph.microsoft.com/beta/groups?`$filter=displayName eq '$autopilotGroupName'" try { $autopilotGroup = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).value Write-Host "found $($autopilotGroup.count)" -f Yellow $autopilotGroupId = $autopilotGroup.Id } catch { Write-Host 'failed' -f Red throw $_ } Write-Host "`tAssign Group to policy" write-host "`t`tPolicy: $($deviceConfigurations.id)" Write-Host "`t`tGroup : $($autopilotGroupName)" $target = @{ '@odata.type' ='#microsoft.graph.groupAssignmentTarget' "deviceAndAppManagementAssignmentFilterId" = "" "deviceAndAppManagementAssignmentFilterType" = "none" "groupId" = $autopilotGroupId } New3gDeviceManagementDeviceConfigurationAssignment -DeviceConfigurationId $deviceConfigurations.Id -Target $target -ErrorAction Stop #endregion #region: [Entra ] MDM enrollment Write-Host "Entra ID Automatic MDM enrollment" -f Cyan # Policy.ReadWrite.MobilityManagement Write-Host "`tCurrent MDM policy`t" -NoNewline try { $uri = 'https://graph.microsoft.com/beta/policies/mobileDeviceManagementPolicies/0000000a-0000-0000-c000-000000000000?$expand=includedGroups' $policy = Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop } catch { Write-Host 'failed' -f Red throw $_ } $mdmAppliesTo = $policy.appliesTo Write-Host $mdmAppliesTo -f Green foreach ($group in $policy.includedGroups) { Write-Host "`t`tGroup: " -NoNewline if ($group.id -eq $mdmGroup.Id) { Write-Host "$($group.displayName) [$($group.id)]" -f Green } else { Write-Host "$($group.displayName) [$($group.id)]" } } if ( ($mdmAppliesTo -ne 'all') -and ($mdmGroup.Id -notin $policy.includedGroups.id) ) { Write-Host "`tUpdate current MDM policy" Write-Host "`t`tScope: Selected" Write-Host "`t`tGroup: $mdmUsersGroupName [$($mdmGroup.id)]`t" -NoNewline #$uri = "https://graph.microsoft.com/beta/groups?`$filter=displayName eq '$mdmUsersGroupName'" $uri = "https://graph.microsoft.com/beta/groups/$($mdmGroup.id)" try { $entraGroup = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop) } catch { Write-Host 'failed' -f Red throw $_ } if ($entraGroup.id) { Write-Host 'found' -f Green Write-Host "`t`tID : $($entraGroup.id)`t" -NoNewline } else { Write-Host 'not found' -f Red throw "Group not found: $mdmGroupName" } $jsonBody = @" { "requests": [ { "id": "1", "method": "POST", "url": "/policies/mobileDeviceManagementPolicies/0000000a-0000-0000-c000-000000000000/includedGroups/`$ref", "body": { "@odata.id": "https://graph.microsoft.com/odata/groups('$($entraGroup.id)')" }, "headers": { "x-ms-command-name": "MDMApplications - AddMdmGroup", "Content-Type": "application/json" } } ] } "@ try { $uri = 'https://graph.microsoft.com/beta/policies/mobileDeviceManagementPolicies/0000000a-0000-0000-c000-000000000000/includedGroups/$ref' $uri = 'https://graph.microsoft.com/beta/$batch' $policy = Invoke-MgGraphRequest -Method POST -Uri $uri -Body $jsonBody -ErrorAction Stop Write-Host 'added' -f Green } catch { Write-Host 'failed' -f Red throw $_ } } #endregion if ($HybridJoin) { #region: [ADDS ] AD User for ODJ-Connector Write-Host "AD Service account for ODJ Connector" -f Cyan <# This connector service account must have the following permissions: Log on as a service Must be part of the Domain user group Must be a member of the local Administrators group on the Windows server that hosts the connectorm Important Managed service accounts aren't supported for the service account. The service account must be a domain account. #> # https://petri.com/creating-active-directory-user-accounts-adsi-powershell/ Write-Host "`tODJ service account" Invoke-Command -Session $sessionDC { $odjSvcAccountName = $using:odjSvcAccountName $ldapPathSvcAcct = $using:ldapPathSvcAcct $passPlain = $using:passPlain $passSecure = $using:passSecure Write-Host "`t`tUser : $odjSvcAccountName`t" -NoNewline try { $objUser = Get-ADUser -Identity $odjSvcAccountName -ErrorAction Stop Write-Host "present" -f Green } catch { Write-Host 'not found' -f Yellow $objUser = $null } if ( -not $objUser) { try { Write-Host "`t`tCreate account`t" -NoNewline $objUser = New-ADUser ` -Path $ldapPathSvcAcct ` -Name $odjSvcAccountName ` -AccountPassword $passSecure ` -Enabled $true ` -ErrorAction Stop Write-Host 'success' -f Green } catch { Write-Host 'failed' -f Red throw $_ } } Write-Host "`t`tPass : " -NoNewline try { # make sure the password has not changed $objUser | Set-ADAccountPassword -NewPassword $passSecure -ErrorAction Stop Write-Host $passPlain -f Green } catch { Write-Host 'failed' -f Red throw $_ } } #endregion #region: [ADDS ] Delegate permissions to Organizational Unit Write-Host "Allow ODJ Connector to add computer accounts" -f Cyan Invoke-Command -Session $sessionDC { $SyncedOU = $using:SyncedOU $odjSvcAccountName = $using:odjSvcAccountName write-Host "`tDelegate AD permissions" write-Host "`t`tOU : $SyncedOU" write-Host "`t`tPermission: Full Control on Computer objects" Write-Host "`t`tUsername : $odjSvcAccountName`t" -NoNewline try { $user = Get-ADuser -Identity $odjSvcAccountName -ErrorAction Stop $userSID = [System.Security.Principal.SecurityIdentifier] $user.SID $identity = [System.Security.Principal.IdentityReference] $userSID $computers = [GUID]"bf967a86-0de6-11d0-a285-00aa003049e2" $resetPassword = [GUID]"00299570-246d-11d0-a768-00aa006e0529" $validatedDNSHostName = [GUID]"72e39547-7b18-11d1-adef-00c04fd8d5cd" $validatedSPN = [GUID]"f3a64788-5306-11d1-a9c5-0000f80367c1" $accountRestrictions = [GUID]"4c164200-20c0-11d0-a768-00aa006e0529" $ruleCreateAndDeleteComputer = New-Object System.DirectoryServices.ActiveDirectoryAccessRule ($identity, "CreateChild, DeleteChild", "Allow", $computers, "All") $ruleResetPassword = New-Object System.DirectoryServices.ActiveDirectoryAccessRule ($identity, "ExtendedRight", "Allow", $resetPassword, "Descendents", $computers) $ruleValidatedDNSHostName = New-Object System.DirectoryServices.ActiveDirectoryAccessRule ($userSID, "Self", "Allow", $validatedDNSHostName, "Descendents", $computers) $ruleValidatedSPN = New-Object System.DirectoryServices.ActiveDirectoryAccessRule ($userSID, "Self", "Allow", $validatedSPN, "Descendents", $computers) $ruleAccountRestrictions = New-Object System.DirectoryServices.ActiveDirectoryAccessRule ($identity, "ReadProperty, WriteProperty", "Allow", $accountRestrictions, "Descendents", $computers) $acl = Get-Acl -Path "AD:\$SyncedOU" -ErrorAction Stop $acl.AddAccessRule($ruleCreateAndDeleteComputer) $acl.AddAccessRule($ruleResetPassword) $acl.AddAccessRule($ruleValidatedDNSHostName) $acl.AddAccessRule($ruleValidatedSPN) $acl.AddAccessRule($ruleAccountRestrictions) Set-Acl -Path "AD:\$SyncedOU" -AclObject $acl -ErrorAction Stop Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } } # Invoke-Command #endregion #region: [Server] Add AD-User-Account to local Administrators on ODJ-Servers Write-Host "Local Administrators on ODJ-Servers" -f Cyan Write-Host "`tAdd User to local group" Write-Host "`t`tServer: $odjConnectorServer" Write-Host "`t`tGroup : Administrators" Write-Host "`t`t Remove invalid member references because of bug " -NoNewline Invoke-Command -Session $sessionSVR -ErrorAction Stop { $domainNBName = $using:domainNBName $odjSvcAccountName = $using:odjSvcAccountName $newMember = "$domainNBName\$odjSvcAccountName" $localGroup = 'Administrators' try { $localAdmins = (@( ([ADSI]"WinNT://./$localGroup").psbase.Invoke('Members') | ForEach-Object { $_.GetType().InvokeMember('AdsPath', 'GetProperty', $null, $($_), $null) } ) -match '^WinNT') -replace 'WinNT://', '' foreach ($member in $localAdmins) { if ($member -like "$env:COMPUTERNAME/*" -or $member -like "$domainNBName/*" -or $member -like "AzureAd/*") { #Write-Host "`t`t`t$member" } else { Remove-LocalGroupMember -group $localGroup -member $member -ErrorAction Stop } } Write-Host 'success' -f Green } catch { Write-Host "failed" -f Red throw $_ } Write-Host "`t`tUser : $newMember`t" -NoNewline try { if ($newMember.Replace('\','/') -notin $localAdmins) { Add-LocalGroupMember -Name 'Administrators' -Member $newMember -ErrorAction Stop Write-Host "success" -f Green } else { Write-Host "present" -f Yellow } } catch { Write-Host "failed" -f Red throw $_ } } #endregion #region: [Server] Assign AD-User-Account 'Logon as a Service' right Write-Host "`tAssign privilege" Write-Host "`t`tPrivilege: Logon as a Service" Write-Host "`t`tComputer : $odjConnectorServer" Write-Host "`t`tAccount : $odjSvcAccountName " -NoNewline Invoke-Command -Session $sessionSVR { Function GrantLogonAsService { [CmdletBinding()] param($accountToAdd) # written by Ingo Karstein, https://blog.kenaro.com/2012/10/12/powershell-script-to-add-account-to-allow-logon-locally-privilege-on-local-security-policy/ # Original: https://gallery.technet.microsoft.com/PowerShell-script-to-add-b005e0f6 # v1.0, 01/03/2014 ## <--- Configure here if( [string]::IsNullOrEmpty($accountToAdd) ) { Write-Error "no account specified" exit } ## ---> End of Config $sidstr = $null try { $ntprincipal = new-object System.Security.Principal.NTAccount "$accountToAdd" $sid = $ntprincipal.Translate([System.Security.Principal.SecurityIdentifier]) $sidstr = $sid.Value.ToString() } catch { $sidstr = $null } Write-Verbose "Account: $($accountToAdd)" if( [string]::IsNullOrEmpty($sidstr) ) { Write-Verbose "Account not found!" exit -1 } Write-Verbose "Account SID: $($sidstr)" $tmp = [System.IO.Path]::GetTempFileName() Write-Verbose "Export current Local Security Policy" $msg = (secedit.exe /export /cfg "$($tmp)") $msg | foreach {Write-Verbose $_} $c = Get-Content -Path $tmp $currentSetting = "" foreach($s in $c) { if( $s -like "SeServiceLogonRight*") { $x = $s.split("=",[System.StringSplitOptions]::RemoveEmptyEntries) $currentSetting = $x[1].Trim() } } if( $currentSetting -notlike "*$($sidstr)*" ) { # Write-Verbose "Modify Setting ""Allow Logon Locally""" Write-Verbose "Modify Setting ""Logon as a Service""" if( [string]::IsNullOrEmpty($currentSetting) ) { $currentSetting = "*$($sidstr)" } else { $currentSetting = "*$($sidstr),$($currentSetting)" } Write-Verbose "$currentSetting" $outfile = @" [Unicode] Unicode=yes [Version] signature="`$CHICAGO`$" Revision=1 [Privilege Rights] SeServiceLogonRight = $($currentSetting) "@ $tmp2 = [System.IO.Path]::GetTempFileName() Write-Verbose "Import new settings to Local Security Policy" $outfile | Set-Content -Path $tmp2 -Encoding Unicode -Force Push-Location (Split-Path $tmp2) try { Write-Verbose "secedit.exe /configure /db ""secedit.sdb"" /cfg ""$($tmp2)"" /areas USER_RIGHTS " $msg = (secedit.exe /configure /db "secedit.sdb" /cfg "$($tmp2)" /areas USER_RIGHTS) $msg | foreach {Write-Verbose $_} } finally { Pop-Location } } else { Write-Verbose "NO ACTIONS REQUIRED! Account already in ""Logon as a Service""" } Write-Verbose "Done" } try { GrantLogonAsService -accountToAdd "$using:domainNBName\$using:odjSvcAccountName" -ErrorAction Stop Write-Host 'success' -f Green } catch { Write-Host 'failed' -f Red throw $_ } } #endregion #region: [Server] Intune Connector Write-Host "Install Intune Connector" -f Cyan Write-Host "`tServer : $odjConnectorServer" try { $svc = Invoke-Command -Session $sessionSVR -ErrorAction Stop -ScriptBlock { Write-Host "`tService : $($using:odjSvcName)`t" -NoNewline Get-Service -ErrorAction Stop | Where-Object Name -eq $using:odjSvcName } } catch { Write-Host 'failed' -f Red throw $_ } if ($svc) { Write-Host 'installed' -f Yellow Write-Host "`tStatus : $($svc.status)" } else { Write-Host 'not found' -f Yellow Write-Host "`tDownload ODJBootstrapper.exe on $odjConnectorServer`t" -NoNewline try { Invoke-Command -Session $sessionSVR -ErrorAction Stop -ScriptBlock { $url = 'https://download.microsoft.com/download/C/6/D/C6DAA9FD-7DCA-4577-9016-AE72A8150149/ODJConnectorBootstrapper.exe' $file = "$env:USERPROFILE\Downloads\ODJConnectorBootstrapper.exe" if (Test-Path $file) { Write-Host "present" -f Yellow } else { Invoke-WebRequest -Uri $url -OutFile $file -ErrorAction Stop Write-Host "success" -f Green } Write-Host "`tPath: $file" } } catch { Write-Host "failed" -f Red throw $_ } Write-Host "`tInstall 'ODJConnectorBootstrapper.exe' on $odjConnectorServer" -f Yellow -b Red #Read-Host "`tPress Enter when ready" } Write-Host "`tConnectors " -NoNewline Do { try { $uri = "https://graph.microsoft.com/beta/deviceManagement/domainJoinConnectors" $connectors = (Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop).value } catch { throw $_ } if ($connectors.count -eq 0) { Write-Host '.' -NoNewline sleep 10 } } Until ($connectors.count -gt 0) Write-Host "found $($connectors.count)" -f Green foreach ($djConnector in $connectors) { if ($djconnector.state -eq 'active') {$fColor = 'Green'} else {$fColor = 'Red'} write-host "`t`tServer: $($djConnector.displayName)`t`tstate: $($djConnector.state)" -f $fColor -NoNewline write-host "`t`t(last sync: $($djConnector.lastConnectionDateTime))" } #endregion #region: [Server] Set Intune Connector service Write-Host "Set Intune Connector service" -f Cyan Write-Host "`tServer : $odjConnectorServer" Write-Host "`tService: $odjSvcName" write-Host "`tStatus : " -NoNewLine $svcStatus = (Invoke-Command -Session $sessionSVR -ScriptBlock { Get-Service -Name $using:odjSvcName -ErrorAction Stop }).Status if ($svcStatus -eq 'Running') { $fColor = 'Green' } else { $fColor = 'Red' } Write-Host $svcStatus -f $fColor Write-Host "`tAccount: $domainNBName\$odjSvcAccountName`t" -NoNewline try { $null = SetServiceAccount -ComputerName $odjConnectorServer -ServiceName $odjSvcName -ConnectionMethod WSMAN -serviceAccountName "$domainNBName\$odjSvcAccountName" -serviceAccountPassword $odjSvcAccountPass Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } Write-Host "`tStartup: Automatic (delayed)`t" -NoNewline try { Invoke-Command -Session $sessionSVR -ErrorAction Stop -ScriptBlock { Set-Service -Name $using:odjSvcName -StartupType Automatic -ErrorAction Stop Write-Host "success" -f Green } } catch { Write-Host "failed" -f Red throw $_ } Write-Host "`tRestart service`t" -NoNewline try { Invoke-Command -Session $sessionSVR -ErrorAction Stop -ScriptBlock { Restart-Service -Name $using:odjSvcName -ErrorAction Stop Write-Host "success" -f Green } } catch { Write-Host "failed" -f Red throw $_ } #endregion #region: [Intune] Device Policy: Domain Join Write-Host "Device Policy: Domain Join" -f Cyan Write-Host "`tCreate Device Configuration Policy" Write-Host "`t`tName : $devicePolicyDJName" Write-Host "`t`tDomain: $domainDnsName" Write-Host "`t`tOU : $SyncedOU" Write-Host "`t`tGet existing policies`t" -NoNewline [array]$intunePolicies = Get-3gIntuneDeviceConfigurationPolicy -ErrorAction Stop | Where displayName -eq $devicePolicyDJName Write-Host "found $($intunePolicies.count)" -f Yellow if ($intunePolicies.count -gt 0) { foreach ($policy in $intunePolicies) { Write-Host "`t`tRemove existing policy: $($policy.id)`t" -NoNewline try { $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations/$($policy.id)" $null = Invoke-MgGraphRequest -Uri $uri -Method DELETE -ErrorAction Stop Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } } # foreach policy } Write-Host "`t`tCreate new policy`t" -NoNewline $jsonBody = @" { "id": "00000000-0000-0000-0000-000000000000", "displayName": "$devicePolicyDJName", "description": "$devicePolicyDJDescr", "roleScopeTagIds": [ "0" ], "@odata.type": "#microsoft.graph.windowsDomainJoinConfiguration", "computerNameStaticPrefix": "HJ-", "activeDirectoryDomainName": "$domainDnsName", "organizationalUnit": "$SyncedOU", "computerNameSuffixRandomCharCount": 12 } "@ try { $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations" $policy = Invoke-MgGraphRequest -Uri $uri -Method POST -Body $jsonBody -ErrorAction Stop Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } Write-Host "`t`tAssign policy" Write-Host "`t`t`tGroup: $($autopilotGroup.DisplayName)`t" -NoNewline $jsonBody = @" { "assignments": [ { "target": { "@odata.type": "#microsoft.graph.groupAssignmentTarget", "groupId": "$($autopilotGroup.id)" } } ] } "@ try { $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations/$($policy.id)/assign" $connectors = (Invoke-MgGraphRequest -Uri $uri -Method POST -Body $jsonBody -ErrorAction Stop).value Write-Host "success" -f Green } catch { Write-Host "failed" -f Red throw $_ } #endregion } #region: [Intune] Prepare Import devices #region: [Intune] Wait for other import activities to finish Write-Host "Import devices (csv file)" -f Cyan Write-Host "`tWait for running imports to finish`t" -NoNewline $count = 0 $savCount = 0 $uri = "https://graph.microsoft.com/beta/deviceManagement/importedWindowsAutopilotDeviceIdentities?`$`select=id,state" Do { $responses = (Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop) if ($responses.value) { $responses = $responses.value } $count = ($responses.state | Where deviceImportStatus -eq 'unknown').Count if (($count -ne 0) -and ($savCount -ne $count)) { Write-Host "[$count]" -f Yellow -NoNewline $savCount = $count } if ($count -gt 0) { Write-Host '.' -NoNewline sleep 5 } } Until ( $count -eq 0 ) Write-Host "[$count]" -f Green #endregion #region: [Intune] Read CSV file Write-Host "`tRead CSV file" While (-not (Test-Path $csvPath) ) { $csvPath = Read-Host "Enter path to Csv-file" } Write-Host "`t`tFile : $csvPath" Write-Host "`t`tImport : " -NoNewline try { $csvImport = (Import-Csv -Path $csvPath -ErrorAction Stop) Write-Host "success" -f Green } catch { Write-Host 'failed' -f Red throw $_ } #endregion foreach ($csvItem in $csvImport) { #region: [Intune] Process CSV items Write-Host "Process CSV-item" -f Cyan $deviceSerNo = $csvItem.'Device Serial Number' Write-Host "`tSerial Nr: $deviceSerNo" if ($csvItem.PSobject.Properties.Name -notcontains 'Group Tag') { try { Write-Host "`t`tAdd missing property: Group Tag`t" -f Yellow -NoNewline Add-Member -InputObject $csvItem -MemberType NoteProperty -Name 'Group Tag' -Value '' -ErrorAction Stop Write-Host 'success' -f Green } catch { Write-Host 'failed' -f Red throw $_ } } Write-Host "`tGroup Tag: " -NoNewline if ($csvItem.'Group Tag' -eq $orderID) { Write-Host $csvItem.'Group Tag' -f Green } else { Write-Host "$($csvItem.'Group Tag') " -NoNewline Write-Host '(invalid for this demo)' -f Yellow $csvItem.'Group Tag' = $orderID Write-Host "`tNew Group Tag applied: " -NoNewline Write-Host "$orderID" -f Green } $deviceGroupTag = $csvItem.'Group Tag' $deviceHash = $csvItem.'Hardware Hash' $deviceProductId = $csvItem.'Windows Product ID' #endregion #region: [Intune] Remove device from Intune Write-Host "Remove previously registered devices" -f Cyan Write-Host "`tGet Intune devices" Write-Host "`t`tSerial Nr : $deviceSerNo`t" -NoNewline try { #[array]$devices = Get-MgDeviceManagementManagedDevice -ErrorAction Stop | where serialNumber -eq $deviceSerNo $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=serialNumber eq '$deviceSerNo'" [array]$intuneDevices = (Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop).value #| Where-Object serialNumber -eq $deviceSerNo Write-Host "found $($intuneDevices.count)" -f Yellow } catch { Write-Host 'failed' -f Red throw $_ } foreach ($device in $intuneDevices) { try { Write-Host "`tRemove Intune device" write-Host "`t`tName: $($device.deviceName)" Write-Host "`t`tID : [$($device.id)] " -NoNewline #Remove-MgDeviceManagementManagedDevice -ManagedDeviceId $device.id -ErrorAction Stop $uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$($device.id)" $null = Invoke-MgGraphRequest -Uri $uri -Method DELETE -ErrorAction Stop write-Host 'deletion initiated' -f Green } catch { Write-Host 'failed' -f Red } } #endregion #region: [Intune] Remove device from Autopilot Write-Host "`tGet Autopilot device" write-host "`t`tSerial Nr: $deviceSerNo`t" -NoNewline try { $uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities" $apDevice = (Invoke-MgGraphRequest -Uri $uri -Method GET -OutputType PSObject -ErrorAction Stop).value | Where serialNumber -eq $deviceSerNo if ($apdevice) { Write-Host "found" -f Green Write-Host "`t`tId : $($apDevice.Id)" Write-Host "`t`tEntra Id : $($apDevice.azureAdDeviceId)" Write-Host "`t`tIntuneId : $($apDevice.managedDeviceId)" Write-Host "`t`tState : $($apDevice.enrollmentState)" Write-Host "`t`tName : $(($intuneDevices | Where-Object id -eq $apDevice.managedDeviceId).deviceName)" Write-Host "`t`tContacted: $($apDevice.lastContactedDateTime)" } else { Write-Host "not found" -f Yellow } } catch { Write-Host 'failed' -f Red throw $_ } if ($apDevice) { Write-Host "`tRemove Autopilot device" try { Write-Host "`t`tID: $($apDevice.id)`t" -NoNewline $uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities/$($apDevice.id)" $null = Invoke-MgGraphRequest -Uri $uri -Method DELETE -ErrorAction Stop Write-Host 'deletion initiated' -f Green } catch { if ($_.ErrorDetails.Message -match 'ZtdDeviceDeletionInProgess') { Write-Host "previous deletion in progress" -f Yellow } else { Write-Host ' failed' -f Red throw $_ } } Write-Host "`t`tWait for deregistration`t" -NoNewline try { # wait for deregistration $uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities" Do { Write-Host "." -NoNewline $autopilotDevice = (Invoke-MgGraphRequest -Uri $uri -Method GET -OutputType PSObject -ErrorAction Stop).value | Where serialNumber -eq $deviceSerNo sleep 5 } While ( $autopilotDevice ) Write-Host ' removed' -f Green } catch { Write-Host ' failed' -f Red throw $_ } } #endregion #region: [Entra ] Get device from Entra ID if ($apDevice) { Write-Host "`tGet Entra device: $($apDevice.azureAdDeviceId)`t" -NoNewline try { $uri = "https://graph.microsoft.com/beta/devices" $entraDevice = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).value | Where-Object deviceId -eq $apDevice.azureAdDeviceId if ($entraDevice) { Write-Host "found" -f Green Write-Host "`t`tDisplayname: $($entraDevice.displayName)" Write-Host "`t`tEnrollment : $($entraDevice.enrollmentType)" } else { Write-Host "not found" -f Yellow } } catch { Write-Host 'failed' -f Red throw $_ } } # $entraDevice | foreach {[pscustomObject]$_} | ogv # $entraDevice.enrollmentType -eq 'OnPremiseCoManaged' # Hybrid-Joined OnPremiseSyncEnabled:True # $entraDevice.enrollmentType -eq 'AzureDomainJoined' # Entra-Joined # $entraDevice.enrollmentType -eq '' # not used #endregion #region: [ADDS ] Remove device from ADDS <# Try { Write-host "Get computer from Active Directory" -NoNewline $adsiSearch = [ADSISearcher]::new() $adsiSearch.Filter = "(sAMAccountName=$Computer`$)" [void]$adsiSearch.PropertiesToLoad.Add("distinguishedName") $computerAccount = $adsiSearch.FindOne() If ($computerAccount) { Write-host "success" -ForegroundColor Green Write-Host "Removing computer from Active Directory`t" -NoNewline $directoryEntry = $ComputerAccount.GetDirectoryEntry() $deletedDevice = $directoryEntry.DeleteTree() Write-Host "success" -f Green } else { Write-Host "failed" -f Red Write-Error "Device not found in Active Directory" } } Catch { Write-Host "failed" -f Red throw $_ } #> if ($entraDevice) { try { Write-Host "`tGet ADDS computer: $($entraDevice.displayName)`t" -NoNewline $adsiSearch = [ADSISearcher]::new() $adsiSearch.Filter = "(sAMAccountName=$($entraDevice.displayName)`$)" $null = $adsiSearch.PropertiesToLoad.Add("distinguishedName") $adComputer = $adsiSearch.FindOne() if ($adComputer) { Write-Host "found" -f Green Write-Host "`tRemove ADDS computer: $($entraDevice.displayName)`t" -NoNewline $directoryEntry = $adComputer.GetDirectoryEntry() $null = $directoryEntry.DeleteTree() write-Host 'success' -f Green } else { Write-Host "not found" -f Yellow } } catch { Write-Host 'failed' -f Red } } #endregion } #endregion #region: [Intune] Import Device into Autopilot write-Host "Import devices into Autopilot" -f Cyan [array]$importedWindowsAutopilotDeviceIdentities = @() foreach ($csvItem in $csvImport) { $ht = @{ serialNumber = $csvItem.'Device Serial Number' productKey = $csvItem.'Windows Product ID' hardwareIdentifier = $csvItem.'Hardware Hash' groupTag = $csvItem.'Group Tag' } if ($assignedUserUpn) { $ht.Add('assignedUserPrincipalName', $assignedUserUpn) } $importedWindowsAutopilotDeviceIdentities += $ht } $hashBody = @{importedWindowsAutopilotDeviceIdentities = $importedWindowsAutopilotDeviceIdentities} $jsonBody = $hashBody | ConvertTo-Json try { Write-Host "`tImport device" Write-Host "`t`tFile : $csvPath`t" -NoNewline $uri = "https://graph.microsoft.com/beta/deviceManagement/importedWindowsAutopilotDeviceIdentities/import" $request = Invoke-MgGraphRequest -Uri $uri -Method POST -Body $jsonBody -ErrorAction Stop Write-Host 'import initiated' -f Green Write-Host "`t`tUser : $assignedUserUpn" } catch { Write-Host 'failed' -f Red throw $_ } [array]$importRegistrationList = @() foreach ($item in $request.value) { Write-Host "`t`tSerial Nr: $($item.serialNumber)`t" -NoNewline $importId = $item.id $uri = "https://graph.microsoft.com/beta/deviceManagement/importedWindowsAutopilotDeviceIdentities/$importId`?`$select=id,state" $importReady = $false Do { Write-Host '.' -NoNewline $responses = Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop if ($responses.state.deviceImportStatus -in 'complete', 'error') { $importReady = $true } else { sleep 5 } } Until ( $importReady ) if ($responses.state.deviceImportStatus -eq 'complete') { Write-Host "`tsuccess" -f Green $importRegistrationList += @{id=$responses.state.deviceRegistrationId;serialNumber=$item.serialNumber} } elseif ($responses.state.deviceImportStatus -eq 'error') { Write-Host "`t$($responses.state.deviceImportStatus)" -f Red Write-Host "`t`tErrorCode: $($responses.state.deviceErrorCode)" Write-Host "`t`tErrorName: $($responses.state.deviceErrorName)" # throw $_ } } #endregion #region: [Intune] Assign Device to Enrollment profile write-host "Assign Enrollment Profile" -f Cyan Write-Host "`tSync : " -NoNewline try { $uri = 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotSettings' $lastSync = (Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop).lastSyncDateTime if ( ((Get-Date) - $lastSync).Minutes -gt 10) { try { $uri = 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotSettings/sync' Invoke-MgGraphRequest -Method POST -Uri $uri -ErrorAction Stop Write-Host "success [$(Get-Date -format 'HH:mm')]" -f Green } catch { Write-Host 'failed' -f Red } } else { Write-Host "last sync $($lastSync.ToString('HH:mm'))" -f Yellow } } catch { Write-Host 'failed' -f Red } Write-Host "`tProfile : $($enrollmentProfileName)" foreach ($importRegistration in $importRegistrationList) { try { # unknown | notAssigned ==> pending ==> (assignedUnkownSyncState) ==> assignedInSync Write-Host "`tSerial Nr: $($importRegistration.serialNumber)`t" -NoNewline $prevStatus = '' Do { $uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities/$($importRegistration.id)`?`$expand=deploymentProfile,intendedDeploymentProfile" $assignments = Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop if ($assignments.deploymentProfileAssignmentStatus -ne $prevStatus) { $prevStatus = $assignments.deploymentProfileAssignmentStatus Write-Host "[$($assignments.deploymentProfileAssignmentStatus)]" -NoNewline -f Yellow } if ($assignments.deploymentProfileAssignmentStatus -ne 'assignedInSync') { Write-Host '.' -NoNewline sleep 10 } } Until ($assignments.deploymentProfileAssignmentStatus -eq 'assignedInSync') Write-Host "`tsuccess" -f Green } catch { Write-Host 'failed' -f Red } } # foreach importRegistrationId #endregion Write-Host "`nScript finished" -f Green Write-Host "`tFile : $invocationInfo" Write-Host "`tDate : $(Get-Date -Format 'yyyy-MM-dd / HH:mm:ss')" Write-Host "`tDuration: $(((Get-Date) -$startTime).ToString('hh\:mm\:ss'))" |