diff --git a/Apps/Get-IntuneiOSManagedAppAssignment.ps1 b/Apps/Get-IntuneiOSManagedAppAssignment.ps1 new file mode 100644 index 0000000..d676371 --- /dev/null +++ b/Apps/Get-IntuneiOSManagedAppAssignment.ps1 @@ -0,0 +1,243 @@ +<# +.SYNOPSIS + List all iOS managed apps assignment information. + +.DESCRIPTION + List all iOS managed apps assignment information. + +.PARAMETER TenantName + Specify the tenant name, e.g. domain.onmicrosoft.com. + +.PARAMETER ApplicationID + Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration. + +.PARAMETER PromptBehavior + Set the prompt behavior when acquiring a token. + +.EXAMPLE + .\Get-IntuneiOSManagedAppAssignment.ps1 -TenantName 'name.onmicrosoft.com' + +.NOTES + FileName: Get-IntuneiOSManagedAppAssignment.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2019-10-01 + Updated: 2019-10-27 + + Version history: + 1.0.0 - (2019-10-01) Script created + 1.0.1 - (2019-10-27) Changed the filter for mobileApps resource to include managed apps too. + + Required modules: + AzureAD (Install-Module -Name AzureAD) + PSIntuneAuth (Install-Module -Name PSIntuneAuth) +#> +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [parameter(Mandatory = $true, HelpMessage = "Specify the tenant name, e.g. domain.onmicrosoft.com.")] + [ValidateNotNullOrEmpty()] + [string]$TenantName, + + [parameter(Mandatory = $false, HelpMessage = "Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration.")] + [ValidateNotNullOrEmpty()] + [string]$ApplicationID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547", + + [parameter(Mandatory=$false, HelpMessage="Set the prompt behavior when acquiring a token.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Auto", "Always", "Never", "RefreshSession")] + [string]$PromptBehavior = "Auto" +) +Begin { + # Determine if the PSIntuneAuth module needs to be installed + try { + Write-Verbose -Message "Attempting to locate PSIntuneAuth module" + $PSIntuneAuthModule = Get-InstalledModule -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false + if ($PSIntuneAuthModule -ne $null) { + Write-Verbose -Message "Authentication module detected, checking for latest version" + $LatestModuleVersion = (Find-Module -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false).Version + if ($LatestModuleVersion -gt $PSIntuneAuthModule.Version) { + Write-Verbose -Message "Latest version of PSIntuneAuth module is not installed, attempting to install: $($LatestModuleVersion.ToString())" + $UpdateModuleInvocation = Update-Module -Name PSIntuneAuth -Scope CurrentUser -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + } + } + catch [System.Exception] { + Write-Warning -Message "Unable to detect PSIntuneAuth module, attempting to install from PSGallery" + try { + # Install NuGet package provider + $PackageProvider = Install-PackageProvider -Name NuGet -Force -Verbose:$false + + # Install PSIntuneAuth module + Install-Module -Name PSIntuneAuth -Scope AllUsers -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + Write-Verbose -Message "Successfully installed PSIntuneAuth" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to install PSIntuneAuth module. Error message: $($_.Exception.Message)" ; break + } + } + + # Check if token has expired and if, request a new + Write-Verbose -Message "Checking for existing authentication token" + if ($Global:AuthToken -ne $null) { + $UTCDateTime = (Get-Date).ToUniversalTime() + $TokenExpireMins = ($Global:AuthToken.ExpiresOn.datetime - $UTCDateTime).Minutes + Write-Verbose -Message "Current authentication token expires in (minutes): $($TokenExpireMins)" + if ($TokenExpireMins -le 0) { + Write-Verbose -Message "Existing token found but has expired, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } + else { + if ($PromptBehavior -like "Always") { + Write-Verbose -Message "Existing authentication token has not expired but prompt behavior was set to always ask for authentication, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } + else { + Write-Verbose -Message "Existing authentication token has not expired, will not request a new token" + } + } + } + else { + Write-Verbose -Message "Authentication token does not exist, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } +} +Process { + # Functions + function Get-ErrorResponseBody { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Exception]$Exception + ) + + # Read the error stream + $ErrorResponseStream = $Exception.Response.GetResponseStream() + $StreamReader = New-Object System.IO.StreamReader($ErrorResponseStream) + $StreamReader.BaseStream.Position = 0 + $StreamReader.DiscardBufferedData() + $ResponseBody = $StreamReader.ReadToEnd() + + # Handle return object + return $ResponseBody + } + + function Invoke-IntuneGraphRequest { + param( + [parameter(Mandatory = $true, ParameterSetName = "Get")] + [parameter(ParameterSetName = "Patch")] + [ValidateNotNullOrEmpty()] + [string]$URI, + + [parameter(Mandatory = $true, ParameterSetName = "Patch")] + [ValidateNotNullOrEmpty()] + [System.Object]$Body + ) + try { + # Construct array list for return values + $ResponseList = New-Object -TypeName System.Collections.ArrayList + + # Call Graph API and get JSON response + switch ($PSCmdlet.ParameterSetName) { + "Get" { + Write-Verbose -Message "Current Graph API call is using method: Get" + $GraphResponse = Invoke-RestMethod -Uri $URI -Headers $AuthToken -Method Get -ErrorAction Stop -Verbose:$false + if ($GraphResponse -ne $null) { + if ($GraphResponse.value -ne $null) { + foreach ($Response in $GraphResponse.value) { + $ResponseList.Add($Response) | Out-Null + } + } + else { + $ResponseList.Add($GraphResponse) | Out-Null + } + } + } + "Patch" { + Write-Verbose -Message "Current Graph API call is using method: Patch" + $GraphResponse = Invoke-RestMethod -Uri $URI -Headers $AuthToken -Method Patch -Body $Body -ContentType "application/json" -ErrorAction Stop -Verbose:$false + if ($GraphResponse -ne $null) { + foreach ($ResponseItem in $GraphResponse) { + $ResponseList.Add($ResponseItem) | Out-Null + } + } + else { + Write-Warning -Message "Response was null..." + } + } + } + + return $ResponseList + } + catch [System.Exception] { + # Construct stream reader for reading the response body from API call + $ResponseBody = Get-ErrorResponseBody -Exception $_.Exception + + # Handle response output and error message + Write-Output -InputObject "Response content:`n$ResponseBody" + Write-Warning -Message "Request to $($URI) failed with HTTP Status $($_.Exception.Response.StatusCode) and description: $($_.Exception.Response.StatusDescription)" + } + } + + function Get-IntuneManagedApp { + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceAppManagement/mobileApps" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + $GraphResponse = Invoke-IntuneGraphRequest -URI $GraphURI + + # Handle return objects from response + return $GraphResponse + } + + function Get-IntuneManagedAppAssignment { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AppID + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceAppManagement/mobileApps/$($AppID)/assignments" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + $GraphResponse = Invoke-IntuneGraphRequest -URI $GraphURI + + # Handle return objects from response + return $GraphResponse + } + + # Retrieve all managed apps and filter on iOS + $ManagedApps = Get-IntuneManagedApp | Where-Object { $_.'@odata.type' -match "iosVppApp|iosStoreApp|managedIOSStoreApp" } + + # Process each managed app + foreach ($ManagedApp in $ManagedApps) { + Write-Verbose -Message "Attempting to retrieve assignments for managed app: $($ManagedApp.displayName)" + + # Retrieve assignments for current managed iOS app + $ManagedAppAssignments = Get-IntuneManagedAppAssignment -AppID $ManagedApp.id + + # Continue if id property is not null, meaning that there's assignments for the current managed app + if ($ManagedAppAssignments.id -ne $null) { + # Process each assignment for the current managed app + foreach ($ManagedAppAssignment in $ManagedAppAssignments) { + # Construct a custom object for final output of script + $PSObject = [PSCustomObject]@{ + AppName = $ManagedApp.displayName + AppType = $ManagedApp.'@odata.type' + AppID = $ManagedApp.id + AssignmentID = $ManagedAppAssignment.id + UninstallOnDeviceRemoval = $ManagedAppAssignments.settings.uninstallOnDeviceRemoval + } + + # Handle final output + Write-Output -Inputobject $PSObject + } + } + else { + Write-Verbose -Message "Empty query returned for managed app assignments" + } + } +} \ No newline at end of file diff --git a/Apps/Get-StoreAppInformation.ps1 b/Apps/Get-StoreAppInformation.ps1 index 07f9a1c..39c658d 100644 --- a/Apps/Get-StoreAppInformation.ps1 +++ b/Apps/Get-StoreAppInformation.ps1 @@ -1,21 +1,32 @@ <# .SYNOPSIS Search the iTunes or Google Play stores for the app links + .DESCRIPTION This script can search for any app available in either iTunes or Google Play store + .PARAMETER Store Specify which Store to search within + .PARAMETER AppName Specify the app name to search for within the Store + .PARAMETER Limit Limit search results to the specified number (only valid for iTunes Store) + .EXAMPLE .\Get-StoreAppInformation.ps1 -Store iTunes -AppName "Microsoft Word" -Limit 1 + .NOTES - Script name: Get-StoreAppInformation.ps1 + FileName: Get-StoreAppInformation.ps1 Author: Nickolaj Andersen Contact: @NickolajA - DateCreated: 2015-08-19 + Created: 2015-08-19 + Updated: 2019-05-14 + + Version history: + 1.0.0 - (2015-08-19) Script created + 1.0.1 - (2019-05-14) Added BundleId property returned from store search #> [CmdletBinding(SupportsShouldProcess=$true)] param( @@ -23,10 +34,12 @@ param( [ValidateNotNullOrEmpty()] [ValidateSet("iTunes","GooglePlay")] [string]$Store, + [parameter(Mandatory=$true, HelpMessage="Specify the app name to search for within the Store")] [ValidateNotNullOrEmpty()] [ValidatePattern("^[A-Za-z\s]*$")] [string]$AppName, + [parameter(Mandatory=$false, HelpMessage="Limit search results to the specified number (only valid for iTunes Store)")] [ValidateNotNullOrEmpty()] [string]$Limit = "1" @@ -57,6 +70,7 @@ Process { $PSObject = [PSCustomObject]@{ "AppName" = $Object.trackCensoredName "StoreLink" = $Object.trackViewUrl + "BundleId" = $Object.bundleId } Write-Output -InputObject $PSObject } @@ -70,6 +84,7 @@ Process { $PSObject = [PSCustomObject]@{ "AppName" = $Object.innerText "StoreLink" = "https://play.google.com" + $Object.href + "BundleId" = ($Object.href).Split("=")[1] } Write-Output -InputObject $PSObject } diff --git a/Apps/Set-IntuneiOSManagedAppAssignment.ps1 b/Apps/Set-IntuneiOSManagedAppAssignment.ps1 new file mode 100644 index 0000000..9c4bef6 --- /dev/null +++ b/Apps/Set-IntuneiOSManagedAppAssignment.ps1 @@ -0,0 +1,294 @@ +<# +.SYNOPSIS + Update the UninstallOnDeviceRemoval property value to either $true or $false for iOS managed app assignments. + +.DESCRIPTION + Update the UninstallOnDeviceRemoval property value to either $true or $false for iOS managed app assignments. + +.PARAMETER TenantName + Specify the tenant name, e.g. domain.onmicrosoft.com. + +.PARAMETER UninstallOnDeviceRemoval + Specify either True or False to change the Uninstall on device removal app assignment setting. + +.PARAMETER Force + When passed the script will set the UninstallOnDeviceRemoval property value even if it's been set before. + +.PARAMETER ApplicationID + Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration. + +.PARAMETER PromptBehavior + Set the prompt behavior when acquiring a token. + +.EXAMPLE + .\Set-IntuneiOSManagedAppAssignment.ps1 -TenantName 'name.onmicrosoft.com' -UninstallOnDeviceRemoval $true -Force -Verbose + +.NOTES + FileName: Set-IntuneiOSManagedAppAssignment.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2019-10-01 + Updated: 2019-10-27 + + Version history: + 1.0.0 - (2019-10-01) Script created + 1.0.1 - (2019-10-27) Changed the filter for mobileApps resource to include managed apps too. + + Required modules: + AzureAD (Install-Module -Name AzureAD) + PSIntuneAuth (Install-Module -Name PSIntuneAuth) +#> +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [parameter(Mandatory = $true, HelpMessage = "Specify the tenant name, e.g. domain.onmicrosoft.com.")] + [ValidateNotNullOrEmpty()] + [string]$TenantName, + + [parameter(Mandatory = $true, HelpMessage = "Specify either True or False to change the Uninstall on device removal app assignment setting.")] + [ValidateNotNullOrEmpty()] + [bool]$UninstallOnDeviceRemoval, + + [parameter(Mandatory = $false, HelpMessage = "When passed the script will set the UninstallOnDeviceRemoval property value even if it's been set before.")] + [ValidateNotNullOrEmpty()] + [switch]$Force, + + [parameter(Mandatory = $false, HelpMessage = "Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration.")] + [ValidateNotNullOrEmpty()] + [string]$ApplicationID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547", + + [parameter(Mandatory=$false, HelpMessage="Set the prompt behavior when acquiring a token.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Auto", "Always", "Never", "RefreshSession")] + [string]$PromptBehavior = "Auto" +) +Begin { + # Determine if the PSIntuneAuth module needs to be installed + try { + Write-Verbose -Message "Attempting to locate PSIntuneAuth module" + $PSIntuneAuthModule = Get-InstalledModule -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false + if ($PSIntuneAuthModule -ne $null) { + Write-Verbose -Message "Authentication module detected, checking for latest version" + $LatestModuleVersion = (Find-Module -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false).Version + if ($LatestModuleVersion -gt $PSIntuneAuthModule.Version) { + Write-Verbose -Message "Latest version of PSIntuneAuth module is not installed, attempting to install: $($LatestModuleVersion.ToString())" + $UpdateModuleInvocation = Update-Module -Name PSIntuneAuth -Scope CurrentUser -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + } + } + catch [System.Exception] { + Write-Warning -Message "Unable to detect PSIntuneAuth module, attempting to install from PSGallery" + try { + # Install NuGet package provider + $PackageProvider = Install-PackageProvider -Name NuGet -Force -Verbose:$false + + # Install PSIntuneAuth module + Install-Module -Name PSIntuneAuth -Scope AllUsers -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + Write-Verbose -Message "Successfully installed PSIntuneAuth" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to install PSIntuneAuth module. Error message: $($_.Exception.Message)" ; break + } + } + + # Check if token has expired and if, request a new + Write-Verbose -Message "Checking for existing authentication token" + if ($Global:AuthToken -ne $null) { + $UTCDateTime = (Get-Date).ToUniversalTime() + $TokenExpireMins = ($Global:AuthToken.ExpiresOn.datetime - $UTCDateTime).Minutes + Write-Verbose -Message "Current authentication token expires in (minutes): $($TokenExpireMins)" + if ($TokenExpireMins -le 0) { + Write-Verbose -Message "Existing token found but has expired, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } + else { + if ($PromptBehavior -like "Always") { + Write-Verbose -Message "Existing authentication token has not expired but prompt behavior was set to always ask for authentication, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } + else { + Write-Verbose -Message "Existing authentication token has not expired, will not request a new token" + } + } + } + else { + Write-Verbose -Message "Authentication token does not exist, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } +} +Process { + # Functions + function Get-ErrorResponseBody { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Exception]$Exception + ) + + # Read the error stream + $ErrorResponseStream = $Exception.Response.GetResponseStream() + $StreamReader = New-Object System.IO.StreamReader($ErrorResponseStream) + $StreamReader.BaseStream.Position = 0 + $StreamReader.DiscardBufferedData() + $ResponseBody = $StreamReader.ReadToEnd() + + # Handle return object + return $ResponseBody + } + + function Invoke-IntuneGraphRequest { + param( + [parameter(Mandatory = $true, ParameterSetName = "Get")] + [parameter(ParameterSetName = "Patch")] + [ValidateNotNullOrEmpty()] + [string]$URI, + + [parameter(Mandatory = $true, ParameterSetName = "Patch")] + [ValidateNotNullOrEmpty()] + [System.Object]$Body + ) + try { + # Construct array list for return values + $ResponseList = New-Object -TypeName System.Collections.ArrayList + + # Call Graph API and get JSON response + switch ($PSCmdlet.ParameterSetName) { + "Get" { + Write-Verbose -Message "Current Graph API call is using method: Get" + $GraphResponse = Invoke-RestMethod -Uri $URI -Headers $AuthToken -Method Get -ErrorAction Stop -Verbose:$false + if ($GraphResponse -ne $null) { + if ($GraphResponse.value -ne $null) { + foreach ($Response in $GraphResponse.value) { + $ResponseList.Add($Response) | Out-Null + } + } + else { + $ResponseList.Add($GraphResponse) | Out-Null + } + } + } + "Patch" { + Write-Verbose -Message "Current Graph API call is using method: Patch" + $GraphResponse = Invoke-RestMethod -Uri $URI -Headers $AuthToken -Method Patch -Body $Body -ContentType "application/json" -ErrorAction Stop -Verbose:$false + if ($GraphResponse -ne $null) { + foreach ($ResponseItem in $GraphResponse) { + $ResponseList.Add($ResponseItem) | Out-Null + } + } + else { + Write-Warning -Message "Response was null..." + } + } + } + + return $ResponseList + } + catch [System.Exception] { + # Construct stream reader for reading the response body from API call + $ResponseBody = Get-ErrorResponseBody -Exception $_.Exception + + # Handle response output and error message + Write-Output -InputObject "Response content:`n$ResponseBody" + Write-Warning -Message "Request to $($URI) failed with HTTP Status $($_.Exception.Response.StatusCode) and description: $($_.Exception.Response.StatusDescription)" + } + } + + function Get-IntuneManagedApp { + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceAppManagement/mobileApps" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + $GraphResponse = Invoke-IntuneGraphRequest -URI $GraphURI + + # Handle return objects from response + return $GraphResponse + } + + function Get-IntuneManagedAppAssignment { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AppID + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceAppManagement/mobileApps/$($AppID)/assignments" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + $GraphResponse = Invoke-IntuneGraphRequest -URI $GraphURI + + # Handle return objects from response + return $GraphResponse + } + + function Set-IntuneManagedAppAssignment { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AppID, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AssignmentID, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Object]$Body + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceAppManagement/mobileApps/$($AppID)/assignments/$($AssignmentID)" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + $GraphResponse = Invoke-IntuneGraphRequest -URI $GraphURI -Body $Body + + # Handle return objects from response + return $GraphResponse + } + + # Retrieve all managed apps and filter on iOS + $ManagedApps = Get-IntuneManagedApp | Where-Object { $_.'@odata.type' -match "iosVppApp|iosStoreApp|managedIOSStoreApp" } + + # Process each managed app + foreach ($ManagedApp in $ManagedApps) { + Write-Verbose -Message "Attempting to retrieve assignments for managed app: $($ManagedApp.displayName)" + + # Retrieve assignments for current managed iOS app + $ManagedAppAssignments = Get-IntuneManagedAppAssignment -AppID $ManagedApp.id + + # Continue if id property is not null, meaning that there's assignments for the current managed app + if ($ManagedAppAssignments.id -ne $null) { + Write-Verbose -Message "Detected assignments for current managed app" + + foreach ($ManagedAppAssignment in $ManagedAppAssignments) { + # Handle uninstall at device removal value + if ($ManagedAppAssignment.settings.uninstallOnDeviceRemoval -eq $null) { + Write-Verbose -Message "Detected empty property value for uninstall at device removal, updating property value" + $ManagedAppAssignment.settings.uninstallOnDeviceRemoval = $UninstallOnDeviceRemoval + } + + # Force update non-set property values + if ($PSBoundParameters["Force"]) { + $ManagedAppAssignment.settings.uninstallOnDeviceRemoval = $UninstallOnDeviceRemoval + } + + # Construct JSON object for POST call + $JSONTable = @{ + 'id' = $ManagedAppAssignment.id + 'settings' = $ManagedAppAssignment.settings + } + $JSONData = $JSONTable | ConvertTo-Json + + # Call Graph API post operation with updated settings values for assignment + Write-Verbose -Message "Attempting to update uninstallOnDeviceRemoval for assignment ID '$($ManagedAppAssignment.id)' with value: $($UninstallOnDeviceRemoval)" + $Invocation = Set-IntuneManagedAppAssignment -AppID $ManagedApp.id -AssignmentID $ManagedAppAssignment.id -Body $JSONData + } + } + else { + Write-Verbose -Message "Empty query returned for managed app assignments" + } + } +} \ No newline at end of file diff --git a/Apps/Visual C++/Get-VCRedistDetection.ps1 b/Apps/Visual C++/Get-VCRedistDetection.ps1 new file mode 100644 index 0000000..8de0192 --- /dev/null +++ b/Apps/Visual C++/Get-VCRedistDetection.ps1 @@ -0,0 +1,87 @@ +# Define the Azure Storage blob URL for where the VcRedist.json file can be accessed +$VcRedistJSONUri = "https://" + +try { + # Construct initial table for detection values for all Visual C++ applications populated from JSON file + $VcRedistTable = New-Object -TypeName "System.Collections.Hashtable" + $VcRedistMetaData = Invoke-RestMethod -Uri $VcRedistJSONUri -ErrorAction Stop + foreach ($VcRedistItem in $VcRedistMetaData.VCRedist) { + $KeyName = -join($VcRedistItem.Version.Replace("-", ""), $VcRedistItem.Architecture) + $VcRedistTable.Add($KeyName, $false) + } +} +catch [System.Exception] { + # Error catched but output is not being redirected, as it would confuse the Win32 app detection model +} + +# Construct list for holding detected Visual C++ applications from registry lookup +$VcRedistUninstallList = New-Object -TypeName "System.Collections.ArrayList" + +# Define Uninstall registry paths for both 32-bit and 64-bit +$UninstallNativePath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" +$UninstallWOW6432Path = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + +# Add all uninstall registry entries to list from native path +$UninstallItemList = New-Object -TypeName "System.Collections.ArrayList" +$UninstallNativeItems = Get-ChildItem -Path $UninstallNativePath -ErrorAction SilentlyContinue +if ($UninstallNativeItems -ne $null) { + $UninstallItemList.AddRange($UninstallNativeItems) | Out-Null +} + +# Add all uninstall registry entries to list from Wow6432Node path +$UninstallWOW6432Items = Get-ChildItem -Path $UninstallWOW6432Path -ErrorAction SilentlyContinue +if ($UninstallWOW6432Items -ne $null) { + $UninstallItemList.AddRange($UninstallWOW6432Items) | Out-Null +} + +# Determine the detection rules for applicable Visual C++ application installations for operating system architecture +$Is64BitOperatingSystem = [System.Environment]::Is64BitOperatingSystem +if ($Is64BitOperatingSystem -eq $true) { + # Construct new detection table to hold detection values for all Visual C++ applications + $VcRedistDetectionTable = New-Object -TypeName "System.Collections.Hashtable" + foreach ($VcRedistTableItem in $VcRedistTable.Keys) { + $VcRedistDetectionTable.Add($VcRedistTableItem, $VcRedistTable[$VcRedistTableItem]) + } +} +else { + # Construct new detection table to hold detection values for all Visual C++ applications + $VcRedistDetectionTable = New-Object -TypeName "System.Collections.Hashtable" + foreach ($VcRedistTableItem in $VcRedistTable.Keys) { + if ($VcRedistTableItem -match "x86") { + $VcRedistDetectionTable.Add($VcRedistTableItem, $VcRedistTable[$VcRedistTableItem]) + } + } +} + +# Process each uninstall registry item from list +foreach ($VcRedistItem in $UninstallItemList) { + try { + $DisplayName = Get-ItemPropertyValue -Path $VcRedistItem.PSPath -Name "DisplayName" -ErrorAction Stop + if (($DisplayName -match "^Microsoft Visual C\+\+\D*(?(\d|-){4,9}).*Redistributable.*(?(x86|x64)).*") -or ($DisplayName -match "^Microsoft Visual C\+\+\D*(?(\d|-){4,9}).*(?(x86|x64)).*Redistributable.*")) { + $PSObject = [PSCustomObject]@{ + "DisplayName" = $DisplayName + "Version" = (Get-ItemPropertyValue -Path $VcRedistItem.PSPath -Name "DisplayVersion") + "Architecture" = $Matches.Architecture + "Year" = $Matches.Year.Replace("-", "") + "Path" = $VcRedistItem.PSPath + } + $VcRedistUninstallList.Add($PSObject) | Out-Null + } + } + catch [System.Exception] { + # Error catched but output is not being redirected, as it would confuse the Win32 app detection model + } +} + +# Set detection value in hash-table for each detected Visual C++ application +foreach ($VcRedistApp in $VcRedistUninstallList) { + $DetectionItemName = -join($VcRedistApp.Year, $VcRedistApp.Architecture) + if ($VcRedistDetectionTable.Keys -contains $DetectionItemName) { + $VcRedistDetectionTable[$DetectionItemName] = $true + } +} + +# Handle final detection logic, return only if all desired Visual C++ applications was found +if ($VcRedistDetectionTable.Values -notcontains $false) { + Write-Output -InputObject "Application detected" +} \ No newline at end of file diff --git a/Apps/Visual C++/Install-VCRedist.ps1 b/Apps/Visual C++/Install-VCRedist.ps1 new file mode 100644 index 0000000..aa89d37 --- /dev/null +++ b/Apps/Visual C++/Install-VCRedist.ps1 @@ -0,0 +1,162 @@ +<# +.SYNOPSIS + Install Visual C++ Redistributable applications defined in the specified JSON master file. + +.DESCRIPTION + Install Visual C++ Redistributable applications defined in the specified JSON master file. + +.PARAMETER URL + Specify the Azure Storage blob URL where JSON file is accessible from. + +.EXAMPLE + # Install all Visual C++ Redistributable applications defined in a JSON file published at a given URL: + .\Install-VisualCRedist.ps1 -URL "https://" + +.NOTES + FileName: Install-VisualCRedist.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-02-05 + Updated: 2020-02-05 + + Version history: + 1.0.0 - (2020-02-05) Script created +#> +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [parameter(Mandatory = $false, HelpMessage = "Specify the Azure Storage blob URL where JSON file is accessible from.")] + [ValidateNotNullOrEmpty()] + [string]$URL = "https://" +) +Process { + # Functions + function Write-LogEntry { + param ( + [parameter(Mandatory = $true, HelpMessage = "Value added to the log file.")] + [ValidateNotNullOrEmpty()] + [string]$Value, + + [parameter(Mandatory = $true, HelpMessage = "Severity for the log entry. 1 for Informational, 2 for Warning and 3 for Error.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("1", "2", "3")] + [string]$Severity, + + [parameter(Mandatory = $false, HelpMessage = "Name of the log file that the entry will written to.")] + [ValidateNotNullOrEmpty()] + [string]$FileName = "VisualCRedist.log" + ) + # Determine log file location + $LogFilePath = Join-Path -Path $env:SystemRoot -ChildPath (Join-Path -Path "Temp" -ChildPath $FileName) + + # Construct time stamp for log entry + $Time = -join @((Get-Date -Format "HH:mm:ss.fff"), "+", (Get-WmiObject -Class Win32_TimeZone | Select-Object -ExpandProperty Bias)) + + # Construct date for log entry + $Date = (Get-Date -Format "MM-dd-yyyy") + + # Construct context for log entry + $Context = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) + + # Construct final log entry + $LogText = "" + + # Add value to log file + try { + Out-File -InputObject $LogText -Append -NoClobber -Encoding Default -FilePath $LogFilePath -ErrorAction Stop + } + catch [System.Exception] { + Write-Warning -Message "Unable to append log entry to VisualCRedist.log file. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" + } + } + + function Invoke-Executable { + param ( + [parameter(Mandatory = $true, HelpMessage = "Specify the file name or path of the executable to be invoked, including the extension.")] + [ValidateNotNullOrEmpty()] + [string]$FilePath, + + [parameter(Mandatory = $false, HelpMessage = "Specify arguments that will be passed to the executable.")] + [ValidateNotNull()] + [string]$Arguments + ) + + # Construct a hash-table for default parameter splatting + $SplatArgs = @{ + FilePath = $FilePath + NoNewWindow = $true + Passthru = $true + ErrorAction = "Stop" + } + + # Add ArgumentList param if present + if (-not ([System.String]::IsNullOrEmpty($Arguments))) { + $SplatArgs.Add("ArgumentList", $Arguments) + } + + # Invoke executable and wait for process to exit + try { + $Invocation = Start-Process @SplatArgs + $Handle = $Invocation.Handle + $Invocation.WaitForExit() + } + catch [System.Exception] { + Write-Warning -Message $_.Exception.Message; break + } + + # Handle return value with exitcode from process + return $Invocation.ExitCode + } + + Write-LogEntry -Value "Starting installation of Visual C++ applications" -Severity 1 + + try { + # Load JSON meta data from Azure Storage blob file + Write-LogEntry -Value "Loading meta data from URL: $($URL)" -Severity 1 + $VcRedistMetaData = Invoke-RestMethod -Uri $URL -ErrorAction Stop + } + catch [System.Exception] { + Write-Warning -Message "Failed to access JSON file. Error message: $($_.Exception.Message)"; break + } + + # Set install root path based on current working directory + $InstallRootPath = Join-Path -Path $PSScriptRoot -ChildPath "Source" + + # Get current architecture of operating system + $Is64BitOperatingSystem = [System.Environment]::Is64BitOperatingSystem + + # Process each item from JSON meta data + foreach ($VcRedistItem in $VcRedistMetaData.VCRedist) { + if (($Is64BitOperatingSystem -eq $false) -and ($VcRedistItem.Architecture -like "x64")) { + Write-LogEntry -Value "Skipping installation of '$($VcRedistItem.Architecture)' for '$($VcRedistItem.DisplayName)' on a non 64-bit operating system" -Severity 2 + } + else { + Write-LogEntry -Value "Processing item for installation: $($VcRedistItem.DisplayName)" -Severity 1 + + # Determine execution path for current item + $FileExecutionPath = Join-Path -Path $InstallRootPath -ChildPath (Join-Path -Path $VcRedistItem.Version -ChildPath (Join-Path -Path $VcRedistItem.Architecture -ChildPath $VcRedistItem.FileName)) + Write-LogEntry -Value "Determined file execution path for current item: $($FileExecutionPath)" -Severity 1 + + # Install current executable + if (Test-Path -Path $FileExecutionPath) { + Write-LogEntry -Value "Starting installation of: $($VcRedistItem.DisplayName)" -Severity 1 + $Invocation = Invoke-Executable -FilePath $FileExecutionPath -Arguments $VcRedistItem.Parameters + + switch ($Invocation) { + 0 { + Write-LogEntry -Value "Successfully installed application" -Severity 1 + } + 3010 { + Write-LogEntry -Value "Successfully installed application, but a restart is required" -Severity 1 + } + default { + Write-LogEntry -Value "Failed to install application, exit code: $($Invocation)" -Severity 3 + } + } + } + else { + Write-LogEntry -Value "Unable to detect file executable for: $($VcRedistItem.DisplayName)" -Severity 3 + Write-LogEntry -Value "Expected file could not be found: $($FileExecutionPath)" -Severity 3 + } + } + } +} \ No newline at end of file diff --git a/Apps/Visual C++/Save-VCRedist.ps1 b/Apps/Visual C++/Save-VCRedist.ps1 new file mode 100644 index 0000000..2973859 --- /dev/null +++ b/Apps/Visual C++/Save-VCRedist.ps1 @@ -0,0 +1,96 @@ +<# +.SYNOPSIS + Download Visual C++ Redistributable executables defined in the specified JSON master file. + +.DESCRIPTION + Download Visual C++ Redistributable executables defined in the specified JSON master file. + All files will be downloaded into a folder named Source that will be created automatically in the executing directory of the script. + +.PARAMETER URL + Specify the Azure Storage blob URL where JSON file is accessible from. + +.EXAMPLE + # Download all Visual C++ Redistributable executables defined in a JSON file published at a given URL: + .\Save-VCRedist.ps1 -URL "https://" + +.NOTES + FileName: Save-VisualCRedist.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-02-05 + Updated: 2020-02-05 + + Version history: + 1.0.0 - (2020-02-05) Script created +#> +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [parameter(Mandatory = $false, HelpMessage = "Specify the Azure Storage blob URL where JSON file is accessible from.")] + [ValidateNotNullOrEmpty()] + [string]$URL = "https://" +) +Process { + # Functions + function Start-DownloadFile { + param( + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$URL, + + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Path, + + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Name + ) + Begin { + # Construct WebClient object + $WebClient = New-Object -TypeName System.Net.WebClient + } + Process { + # Create path if it doesn't exist + if (-not(Test-Path -Path $Path)) { + New-Item -Path $Path -ItemType Directory -Force | Out-Null + } + + # Start download of file + $WebClient.DownloadFile($URL, (Join-Path -Path $Path -ChildPath $Name)) + } + End { + # Dispose of the WebClient object + $WebClient.Dispose() + } + } + + try { + # Load JSON meta data from Azure Storage blob file + Write-Verbose -Message "Loading meta data from URL: $($URL)" + $VcRedistMetaData = Invoke-RestMethod -Uri $URL -ErrorAction Stop + } + catch [System.Exception] { + Write-Warning -Message "Failed to access JSON file. Error message: $($_.Exception.Message)"; break + } + + # Set download path based on current working directory + $DownloadRootPath = Join-Path -Path $PSScriptRoot -ChildPath "Source" + + # Process each item from JSON meta data + foreach ($VcRedistItem in $VcRedistMetaData.VCRedist) { + Write-Verbose -Message "Processing item: $($VcRedistItem.DisplayName)" + + # Determine download path for current item + $DownloadPath = Join-Path -Path $DownloadRootPath -ChildPath (Join-Path -Path $VcRedistItem.Version -ChildPath $VcRedistItem.Architecture) + Write-Verbose -Message "Determined download path for current item: $($DownloadPath)" + + # Create download path if it doesn't exist + if (-not(Test-Path -Path $DownloadPath)) { + New-Item -Path $DownloadPath -ItemType Directory -Force | Out-Null + } + + # Start download of current item + Start-DownloadFile -Path $DownloadPath -URL $VcRedistItem.URL -Name $VcRedistItem.FileName + Write-Verbose -Message "Successfully downloaded: $($VcRedistItem.DisplayName)" + } +} \ No newline at end of file diff --git a/Apps/Visual C++/VCRedist.json b/Apps/Visual C++/VCRedist.json new file mode 100644 index 0000000..cbc9859 --- /dev/null +++ b/Apps/Visual C++/VCRedist.json @@ -0,0 +1,84 @@ +{ + "VCRedist": [ + { + "DisplayName": "Visual C++ 2008 Service Pack 1 Redistributable Package MFC Security Update", + "URL": "https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe", + "FileName": "vcredist_x64.exe", + "Version": "2008", + "Architecture": "x64", + "Parameters": "/Q" + }, + { + "DisplayName": "Visual C++ 2008 Service Pack 1 Redistributable Package MFC Security Update", + "URL": "https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe", + "FileName": "vcredist_x86.exe", + "Version": "2008", + "Architecture": "x86", + "Parameters": "/Q" + }, + { + "DisplayName": "Visual C++ 2010 Service Pack 1 Redistributable Package MFC Security Update", + "URL": "https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe", + "FileName": "vcredist_x64.exe", + "Version": "2010", + "Architecture": "x64", + "Parameters": "/quiet /norestart" + }, + { + "DisplayName": "Visual C++ 2010 Service Pack 1 Redistributable Package MFC Security Update", + "URL": "https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe", + "FileName": "vcredist_x86.exe", + "Version": "2010", + "Architecture": "x86", + "Parameters": "/quiet /norestart" + }, + { + "DisplayName": "Visual C++ Redistributable for Visual Studio 2012 Update 4", + "URL": "https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe", + "FileName": "vcredist_x64.exe", + "Version": "2012", + "Architecture": "x64", + "Parameters": "/quiet /norestart" + }, + { + "DisplayName": "Visual C++ Redistributable for Visual Studio 2012 Update 4", + "URL": "https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe", + "FileName": "vcredist_x86.exe", + "Version": "2012", + "Architecture": "x86", + "Parameters": "/install /quiet /norestart" + }, + { + "DisplayName": "Visual C++ 2013 Update 5 Redistributable Package", + "URL": "https://download.visualstudio.microsoft.com/download/pr/10912041/cee5d6bca2ddbcd039da727bf4acb48a/vcredist_x64.exe", + "FileName": "vcredist_x64.exe", + "Version": "2013", + "Architecture": "x64", + "Parameters": "/install /quiet /norestart" + }, + { + "DisplayName": "Visual C++ 2013 Update 5 Redistributable Package", + "URL": "https://download.visualstudio.microsoft.com/download/pr/10912113/5da66ddebb0ad32ebd4b922fd82e8e25/vcredist_x86.exe", + "FileName": "vcredist_x86.exe", + "Version": "2013", + "Architecture": "x86", + "Parameters": "/install /quiet /norestart" + }, + { + "DisplayName": "Visual C++ Redistributable for Visual Studio 2015-2019", + "URL": "https://aka.ms/vs/16/release/vc_redist.x64.exe", + "FileName": "vc_redist.x64.exe", + "Version": "2015-2019", + "Architecture": "x64", + "Parameters": "/install /quiet /norestart" + }, + { + "DisplayName": "Visual C++ Redistributable for Visual Studio 2015-2019", + "URL": "https://aka.ms/vs/16/release/vc_redist.x86.exe", + "FileName": "vc_redist.x86.exe", + "Version": "2015-2019", + "Architecture": "x86", + "Parameters": "/install /quiet /norestart" + } + ] +} \ No newline at end of file diff --git a/Automation/Get-AppleMDMPushCertificateExpiration.ps1 b/Automation/Get-AppleMDMPushCertificateExpiration.ps1 new file mode 100644 index 0000000..c575ff1 --- /dev/null +++ b/Automation/Get-AppleMDMPushCertificateExpiration.ps1 @@ -0,0 +1,127 @@ +# Functions +function Send-O365MailMessage { + param ( + [parameter(Mandatory=$true)] + [string]$Credential, + [parameter(Mandatory=$false)] + [string]$Body, + [parameter(Mandatory=$false)] + [string]$Subject, + [parameter(Mandatory=$true)] + [string]$Recipient, + [parameter(Mandatory=$true)] + [string]$From + ) + # Get Azure Automation credential for authentication + $PSCredential = Get-AutomationPSCredential -Name $Credential + + # Construct the MailMessage object + $MailMessage = New-Object -TypeName System.Net.Mail.MailMessage + $MailMessage.From = $From + $MailMessage.ReplyTo = $From + $MailMessage.To.Add($Recipient) + $MailMessage.Body = $Body + $MailMessage.BodyEncoding = ([System.Text.Encoding]::UTF8) + $MailMessage.IsBodyHtml = $true + $MailMessage.SubjectEncoding = ([System.Text.Encoding]::UTF8) + + # Attempt to set the subject + try { + $MailMessage.Subject = $Subject + } + catch [System.Management.Automation.SetValueInvocationException] { + Write-Warning -InputObject "An exception occurred while setting the message subject" + } + + # Construct SMTP Client object + $SMTPClient = New-Object -TypeName System.Net.Mail.SmtpClient -ArgumentList @("smtp.office365.com", 587) + $SMTPClient.Credentials = $PSCredential + $SMTPClient.EnableSsl = $true + + # Send mail message + $SMTPClient.Send($MailMessage) +} + +# Define email information details +$AzureAutomationCredentialName = "MailUser" +$MailRecipient = "recipient@domain.com" +$MailFrom = "user@domain.com" + +# Define Azure Automation variables +$AzureAutomationCredentialName = "MSIntuneAutomationUser" +$AzureAutomationVariableAppClientID = "AppClientID" +$AzureAutomationVariableTenantName = "TenantName" + +# Define monitoring options +$AppleMDMPushCertificateNotificationRange = 7 + +try { + # Import required modules + Write-Output -InputObject "Importing required modules" + Import-Module -Name AzureAD -ErrorAction Stop + Import-Module -Name PSIntuneAuth -ErrorAction Stop + + try { + # Read credentials and variables + Write-Output -InputObject "Reading automation variables" + $Credential = Get-AutomationPSCredential -Name $AzureAutomationCredentialName -ErrorAction Stop + $AppClientID = Get-AutomationVariable -Name $AzureAutomationVariableAppClientID -ErrorAction Stop + $TenantName = Get-AutomationVariable -Name $AzureAutomationVariableTenantName -ErrorAction Stop + + try { + # Retrieve authentication token + Write-Output -InputObject "Attempting to retrieve authentication token" + $AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $AppClientID -Credential $Credential -ErrorAction Stop + if ($AuthToken -ne $null) { + Write-Output -InputObject "Successfully retrieved authentication token" + + try { + # Get Apple MDM Push certificates + $AppleMDMPushResource = "https://graph.microsoft.com/v1.0/devicemanagement/applePushNotificationCertificate" + $AppleMDMPushCertificate = Invoke-RestMethod -Uri $AppleMDMPushResource -Method Get -Headers $AuthToken -ErrorAction Stop + + if ($AppleMDMPushCertificate -ne $null) { + Write-Output -InputObject "Successfully retrieved Apple MDM Push certificate" + + # Parse the JSON date time string into an DateTime object + $AppleMDMPushCertificateExpirationDate = [System.DateTime]::Parse($AppleMDMPushCertificate.expirationDateTime) + + # Validate that the MDM Push certificate has not already expired + if ($AppleMDMPushCertificateExpirationDate -lt (Get-Date)) { + Write-Output -InputObject "Apple MDM Push certificate has already expired, sending notification email" + Send-O365MailMessage -Credential $AzureAutomationCredentialName -Body "ACTION REQUIRED: Apple MDM Push certificate has expired" -Subject "MSIntune: IMPORTANT - Apple MDM Push certificate has expired" -Recipient $MailRecipient -From $MailFrom + } + else { + $AppleMDMPushCertificateDaysLeft = ($AppleMDMPushCertificateExpirationDate - (Get-Date)) + if ($AppleMDMPushCertificateDaysLeft.Days -le $AppleMDMPushCertificateNotificationRange) { + Write-Output -InputObject "Apple MDM Push certificate has not expired, but is within the given expiration notification range" + Send-O365MailMessage -Credential $AzureAutomationCredentialName -Body "Please take action before the Apple MDM Push certificate expires" -Subject "MSIntune: Apple MDM Push certificate expires in $($AppleMDMPushCertificateDaysLeft.Days) days" -Recipient $MailRecipient -From $MailFrom + } + else { + Write-Output -InputObject "Apple MDM Push certificate has not expired and is outside of the specified expiration notification range" + } + } + } + else { + Write-Output -InputObject "Query for Apple MDM Push certificates returned empty" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred. Error message: $($_.Exception.Message)" + } + } + else { + Write-Warning -Message "An error occurred while attempting to retrieve an authentication token" + } + } + catch [System.Exception] { + Write-Warning -Message "Failed to retrieve authentication token" + } + } + catch [System.Exception] { + Write-Warning -Message "Failed to read automation variables" + } +} +catch [System.Exception] { + Write-Warning -Message "Failed to import modules" +} \ No newline at end of file diff --git a/Automation/Monitor-IntuneAppleConnectors.ps1 b/Automation/Monitor-IntuneAppleConnectors.ps1 new file mode 100644 index 0000000..493f7c1 --- /dev/null +++ b/Automation/Monitor-IntuneAppleConnectors.ps1 @@ -0,0 +1,170 @@ +<# +.SYNOPSIS + Monitor all Apple Connectors like Push Notification Certificate, VPP and DEP tokens. + This script is written to be used in an Azure Automation runbook to monitor your Intune deployment connectors. +.DESCRIPTION + Monitor all Apple Connectors like Push Notification Certificate, VPP and DEP tokens. + +.VARIABLES +All variables must be defines in Azure Automation + TenantName + Specify the *.onmicrosoft.com name for your tenant. + AppID + Specify the ClientID of the Azure AD App used for unattended authentication to MS Graph API + AppSecret (encrypted) + Specify the secret key for authentication to the Azure AD App used for unattended authentication to MS Graph (never write that in side the script it self) + ApplicationID + Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration. + Uri + The Uri for the webhook for the Microsoft Teams channel we are sending the alerts too. + +.EXAMPLE + # Script runs unnatended from Azure Automation - all parameters should be defined in Automation account + Monitor-IntuneAppleConnectors.ps1 + +.NOTES + FileName: Monitor-IntuneAppleConnectors.ps1 + Author: Jan Ketil Skanke + Contact: @JankeSkanke + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) First release + + Required modules: + "Microsoft.graph.intune" +#> +#Define Your Notification Ranges +$AppleMDMPushCertNotificationRange = '30' +$AppleVPPTokenNotificationRange = '30' +$AppleDEPTokenNotificationRange = '30' + +# Grab variables frrom automation account - this must match your variable names in Azure Automation Account +# Example $Uri = Get-AutomationVariable -Name "TeamsChannelUri" means the VariableTeamsChannelUri must exist in Azure Automation with the correct variable. +$TenantName = Get-AutomationVariable -Name 'TenantName' +$AppID = Get-AutomationVariable -Name "msgraph-clientcred-appid" +$AppSecret = Get-AutomationVariable -Name "msgraph-clientcred-appsecret" +$Uri = Get-AutomationVariable -Name "TeamsChannelUri" +$Now = Get-Date +Function Send-TeamsAlerts { + [cmdletbinding()] + Param( + [string]$uri, + [string]$ConnectorName, + [string]$ExpirationStatus, + [string]$AppleId, + [string]$ExpDateStr + ) +#Format Message Body for Message Card in Microsoft Teams +$body = @" +{ + "@type": "MessageCard", + "@context": "https://schema.org/extensions", + "summary": "Intune Apple Notification", + "themeColor": "ffff00", + "title": "$ExpirationStatus", + "sections": [ + { + "activityTitle": "Warning message", + "activitySubtitle": "$Now", + "activityImage": "https://github.com/JankeSkanke/imagerepo/blob/master/warning.png?raw=true", + "facts": [ + { + "name": "Connector:", + "value": "$ConnectorName" + }, + { + "name": "Status:", + "value": "$ExpirationStatus" + }, + { + "name": "AppleID:", + "value": "$AppleID" + }, + { + "name": "Expiry Date:", + "value": "$ExpDateStr" + } + ], + "text": "Must be renewed by IT Admin before the expiry date." + } + ] +} +"@ +# Post Message Alert to Teams +Invoke-RestMethod -uri $uri -Method Post -body $body -ContentType 'application/json' | Out-Null +Write-Output $ExpirationStatus +} +#Import Modules +import-module "Microsoft.graph.intune" + +# Connect to Intune MSGraph with Client Secret quietly by updating Graph Environment to use our own Azure AD APP and connecting with a ClientSecret +Update-MSGraphEnvironment -SchemaVersion "beta" -AppId $AppId -AuthUrl "https://login.microsoftonline.com/$TenantName" -Quiet +Connect-MSGraph -ClientSecret $AppSecret -Quiet + +# Checking Apple Push Notification Cert +$ApplePushCert = Get-IntuneApplePushNotificationCertificate +$ApplePushCertExpDate = $ApplePushCert.expirationDateTime +$ApplePushIdentifier = $ApplePushCert.appleIdentifier +$APNExpDate = $ApplePushCertExpDate.ToShortDateString() + +if ($ApplePushCertExpDate -lt (Get-Date)) { + $APNExpirationStatus = "MS Intune: Apple MDM Push certificate has already expired" + Send-TeamsAlerts -uri $uri -ConnectorName "Apple Push Notification Certificate" -ExpirationStatus $APNExpirationStatus -AppleId $ApplePushIdentifier -ExpDateStr $APNExpDate +} +else { + $AppleMDMPushCertDaysLeft = ($ApplePushCertExpDate - (Get-Date)) + if ($AppleMDMPushCertDaysLeft.Days -le $AppleMDMPushCertNotificationRange) { + $APNExpirationStatus = "MSIntune: Apple MDM Push certificate expires in $($AppleMDMPushCertDaysLeft.Days) days" + Send-TeamsAlerts -uri $uri -ConnectorName "Apple Push Notification Certificate" -ExpirationStatus $APNExpirationStatus -AppleId $ApplePushIdentifier -ExpDateStr $APNExpDate + } + else { + $APNExpirationStatus = "MSIntune: NOALERT" + Write-Output "APN Certificate OK" + } +} + +# Checking Apple Volume Purchase Program tokens +$AppleVPPToken = Get-DeviceAppManagement_VppTokens + +if($AppleVPPToken.Count -ne '0'){ + foreach ($token in $AppleVPPToken){ + $AppleVPPExpDate = $token.expirationDateTime + $AppleVPPIdentifier = $token.appleId + $AppleVPPState = $token.state + $VPPExpDateStr = $AppleVPPExpDate.ToShortDateString() + if ($AppleVPPState -ne 'valid') { + $VPPExpirationStatus = "MSIntune: Apple VPP Token is not valid, new token required" + Send-TeamsAlerts -uri $uri -ConnectorName "VPP Token" -ExpirationStatus $VPPExpirationStatus -AppleId $AppleVPPIdentifier -ExpDateStr $VPPExpDateStr + } + else { + $AppleVPPTokenDaysLeft = ($AppleVPPExpDate - (Get-Date)) + if ($AppleVPPTokenDaysLeft.Days -le $AppleVPPTokenNotificationRange) {$VPPExpirationStatus = "MSIntune: Apple VPP Token expires in $($AppleVPPTokenDaysLeft.Days) days" + Send-TeamsAlerts -uri $uri -ConnectorName "VPP Token" -ExpirationStatus $VPPExpirationStatus -AppleId $AppleVPPIdentifier -ExpDateStr $VPPExpDateStr + } + else {$VPPExpirationStatus = "MSIntune: NOALERT" + Write-Output "Apple VPP Token OK" + } + } + } +} + +# Checking DEP Token +$AppleDEPToken = (Invoke-MSGraphRequest -Url 'https://graph.microsoft.com/beta/deviceManagement/depOnboardingSettings' -HttpMethod GET).value +if ($AppleDeptoken.Count -ne '0'){ + foreach ($token in $AppleDEPToken){ + $AppleDEPExpDate = $token.tokenExpirationDateTime + $AppleDepID = $token.appleIdentifier + $AppleDEPTokenDaysLeft = ($AppleDEPExpDate - (Get-Date)) + $DEPExpDateStr = $AppleDEPExpDate.ToShortDateString() + if ($AppleDEPTokenDaysLeft.Days -le $AppleDEPTokenNotificationRange) { + $AppleDEPExpirationStatus = "MSIntune: Apple DEP Token expires in $($AppleDEPTokenDaysLeft.Days) days" + Send-TeamsAlerts -uri $uri -ConnectorName "DEP Token" -ExpirationStatus $AppleDEPExpirationStatus -AppleId $AppleDEPId -ExpDateStr $DEPExpDateStr + } + else { + $AppleDEPExpirationStatus = "MSIntune: NOALERT" + Write-Output "Apple DEP Token OK" + } + } +} diff --git a/Automation/bitlockerremedy.ps1 b/Automation/bitlockerremedy.ps1 new file mode 100644 index 0000000..c40b81d --- /dev/null +++ b/Automation/bitlockerremedy.ps1 @@ -0,0 +1,72 @@ +Disable-AzContextAutosave –Scope Process +$connection = Get-AutomationConnection -Name AzureRunAsConnection +$certificate = Get-AutomationCertificate -Name AzureRunAsCertificate +$connectionResult = Connect-AzAccount -ServicePrincipal -Tenant $connection.TenantID -ApplicationId $connection.ApplicationID -CertificateThumbprint $connection.CertificateThumbprint +#write-output $connectionResult + +$GraphConnection = Get-MsalToken -ClientCertificate $certificate -ClientId $connection.ApplicationID -TenantId $connection.TenantID +$Header = @{Authorization = "Bearer $($GraphConnection.AccessToken)"} + +#write-output $GraphConnection + +[string]$WorkspaceID = Get-AutomationVariable -Name 'BitlockerRemedyWorkspaceID' + +#Define my query objects +$ExposedKeysQuery = @' +AuditLogs +| where OperationName == "Read BitLocker key" and TimeGenerated > ago(65m) +| extend MyDetails = tostring(AdditionalDetails[0].value) +| extend userPrincipalName_ = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName) +| parse MyDetails with * "key ID: '" MyRecoveryKeyID "'. Backed up from device: '" MyDevice "'" * +| project MyDevice, MyRecoveryKeyID, userPrincipalName_, TimeGenerated +'@ + +$DeletedKeysQuery = @' +AuditLogs +| where OperationName == "Delete BitLocker key" and TimeGenerated > ago(65m) +| extend MyRecoveryKeyID = tostring(TargetResources[0].displayName) +| project MyRecoveryKeyID, ActivityDateTime +'@ + +$IntuneKeyRolloverQuery = @' +IntuneAuditLogs +| where OperationName == "rotateBitLockerKeys ManagedDevice" and TimeGenerated > ago(65m) +| extend DeviceID = tostring(parse_json(tostring(parse_json(Properties).TargetObjectIds))[0]) +| project DeviceID, ResultType +'@ + +#Query Log Analytics Audit Logs +$AllKeyExposures = Invoke-AZOperationalInsightsQuery -WorkspaceId $WorkspaceID -Query $ExposedKeysQuery +$MyAutoKeyDeletion = Invoke-AZOperationalInsightsQuery -WorkspaceId $WorkspaceID -Query $DeletedKeysQuery +$MyIntuneRolloverActions = Invoke-AZOperationalInsightsQuery -WorkspaceId $WorkspaceID -Query $IntuneKeyRolloverQuery + +$DeviceToRolloverIDs = @() +foreach($KeyExposure in $AllKeyExposures.Results){ + if ($KeyExposure.MyRecoveryKeyID -in $MyAutoKeyDeletion.Results.MyRecoveryKeyID){ + #Write-Output "Device $($KeyExposure.MyDevice) with key $($KeyExposure.MyRecoveryKeyID) has been replaced OK" + }elseif ($KeyExposure -notin $MyAutoKeyDeletion.Results.MyRecoveryKeyID) { + #Write-Output "Device $($KeyExposure.MyDevice) with key $($KeyExposure.MyRecoveryKeyID) needs a rollover" + $DeviceToRolloverIDs += $KeyExposure.MyDevice + } +} + +if ([string]::IsNullOrEmpty($DeviceToRolloverIDs)){ + Write-Output "Query returned empty. Possibly issues with delay in query" + } else { + #Write-Output "Device to rollover IDs $DeviceToRolloverIDs" + foreach($DeviceToRolloverID in $DeviceToRolloverIDs){ + #write-output $DeviceToRolloverID + $GetManagedDeviceIDUri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices?filter=azureADDeviceID eq '$DeviceToRolloverID'" + #Write-Output $GetManagedDeviceIDUri + $ManagedDeviceResult = Invoke-RestMethod -Method GET -Uri $GetManagedDeviceIDUri -ContentType "application/json" -Headers $Header -ErrorAction Stop + write-output "Evaluating $($ManagedDeviceResult.value.deviceName)" + $ManagedDeviceID = $ManagedDeviceResult.value.id + if ($ManagedDeviceID -notin $MyIntuneRolloverActions.Results.DeviceID){ + $RolloverKeyUri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$ManagedDeviceID/rotateBitLockerKeys" + $RolloverKeyResult = Invoke-RestMethod -Method POST -Uri $RolloverKeyUri -ContentType "application/json" -Headers $Header -ErrorAction Stop + write-output "Recovery Key Rollover invoked on $($ManagedDeviceResult.value.deviceName)" + } else { + Write-Output "Intune Rollover has already been performed on $($ManagedDeviceResult.value.deviceName), no action needed" + } + } +} diff --git a/Autopilot/Get-AzureADDeviceRecordsBySerialNumber.ps1 b/Autopilot/Get-AzureADDeviceRecordsBySerialNumber.ps1 new file mode 100644 index 0000000..11d58be --- /dev/null +++ b/Autopilot/Get-AzureADDeviceRecordsBySerialNumber.ps1 @@ -0,0 +1,430 @@ +<# +.SYNOPSIS + Get a list of Azure AD device records that matches the hardware identifier of the associated Azure AD device + object of a device identity in Windows Autopilot based on the serial number as input. + +.DESCRIPTION + This script will retrieve all Azure AD device records that matches the hardware identifier of the associated Azure AD device + object of a device identity in Windows Autopilot based on the serial number as input + +.PARAMETER TenantID + Specify the Azure AD tenant ID. + +.PARAMETER ClientID + Specify the service principal, also known as app registration, Client ID (also known as Application ID). + +.PARAMETER SerialNumber + Specify the serial number of a device known to Windows Autopilot. + +.EXAMPLE + # Retrieve a list of associated Azure AD device records based on hardware identifier by specifying a known serial number in Windows Autopilot: + .\Get-AzureADDeviceRecordsBySerialNumber.ps1 -TenantID "" -ClientID "" -SerialNumber "1234567" + +.NOTES + FileName: Get-AzureADDeviceRecordsBySerialNumber.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2021-03-22 + Updated: 2021-03-22 + + Version history: + 1.0.0 - (2021-03-22) Script created +#> +#Requires -Modules "MSAL.PS" +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [parameter(Mandatory = $true, HelpMessage = "Specify the Azure AD tenant ID.")] + [ValidateNotNullOrEmpty()] + [string]$TenantID, + + [parameter(Mandatory = $true, HelpMessage = "Specify the service principal, also known as app registration, Client ID (also known as Application ID).")] + [ValidateNotNullOrEmpty()] + [string]$ClientID, + + [parameter(Mandatory = $false, HelpMessage = "Specify the serial number of a device known to Windows Autopilot.")] + [ValidateNotNullOrEmpty()] + [string]$SerialNumber +) +Begin {} +Process { + # Functions + function Invoke-MSGraphOperation { + <# + .SYNOPSIS + Perform a specific call to Graph API, either as GET, POST, PATCH or DELETE methods. + + .DESCRIPTION + Perform a specific call to Graph API, either as GET, POST, PATCH or DELETE methods. + This function handles nextLink objects including throttling based on retry-after value from Graph response. + + .PARAMETER Get + Switch parameter used to specify the method operation as 'GET'. + + .PARAMETER Post + Switch parameter used to specify the method operation as 'POST'. + + .PARAMETER Patch + Switch parameter used to specify the method operation as 'PATCH'. + + .PARAMETER Put + Switch parameter used to specify the method operation as 'PUT'. + + .PARAMETER Delete + Switch parameter used to specify the method operation as 'DELETE'. + + .PARAMETER Resource + Specify the full resource path, e.g. deviceManagement/auditEvents. + + .PARAMETER Headers + Specify a hash-table as the header containing minimum the authentication token. + + .PARAMETER Body + Specify the body construct. + + .PARAMETER APIVersion + Specify to use either 'Beta' or 'v1.0' API version. + + .PARAMETER ContentType + Specify the content type for the graph request. + + .NOTES + Author: Nickolaj Andersen & Jan Ketil Skanke + Contact: @JankeSkanke @NickolajA + Created: 2020-10-11 + Updated: 2020-11-11 + + Version history: + 1.0.0 - (2020-10-11) Function created + 1.0.1 - (2020-11-11) Tested in larger environments with 100K+ resources, made small changes to nextLink handling + 1.0.2 - (2020-12-04) Added support for testing if authentication token has expired, call Get-MsalToken to refresh. This version and onwards now requires the MSAL.PS module + #> + param( + [parameter(Mandatory = $true, ParameterSetName = "GET", HelpMessage = "Switch parameter used to specify the method operation as 'GET'.")] + [switch]$Get, + + [parameter(Mandatory = $true, ParameterSetName = "POST", HelpMessage = "Switch parameter used to specify the method operation as 'POST'.")] + [switch]$Post, + + [parameter(Mandatory = $true, ParameterSetName = "PATCH", HelpMessage = "Switch parameter used to specify the method operation as 'PATCH'.")] + [switch]$Patch, + + [parameter(Mandatory = $true, ParameterSetName = "PUT", HelpMessage = "Switch parameter used to specify the method operation as 'PUT'.")] + [switch]$Put, + + [parameter(Mandatory = $true, ParameterSetName = "DELETE", HelpMessage = "Switch parameter used to specify the method operation as 'DELETE'.")] + [switch]$Delete, + + [parameter(Mandatory = $true, ParameterSetName = "GET", HelpMessage = "Specify the full resource path, e.g. deviceManagement/auditEvents.")] + [parameter(Mandatory = $true, ParameterSetName = "POST")] + [parameter(Mandatory = $true, ParameterSetName = "PATCH")] + [parameter(Mandatory = $true, ParameterSetName = "PUT")] + [parameter(Mandatory = $true, ParameterSetName = "DELETE")] + [ValidateNotNullOrEmpty()] + [string]$Resource, + + [parameter(Mandatory = $true, ParameterSetName = "GET", HelpMessage = "Specify a hash-table as the header containing minimum the authentication token.")] + [parameter(Mandatory = $true, ParameterSetName = "POST")] + [parameter(Mandatory = $true, ParameterSetName = "PATCH")] + [parameter(Mandatory = $true, ParameterSetName = "PUT")] + [parameter(Mandatory = $true, ParameterSetName = "DELETE")] + [ValidateNotNullOrEmpty()] + [System.Collections.Hashtable]$Headers, + + [parameter(Mandatory = $true, ParameterSetName = "POST", HelpMessage = "Specify the body construct.")] + [parameter(Mandatory = $true, ParameterSetName = "PATCH")] + [parameter(Mandatory = $true, ParameterSetName = "PUT")] + [ValidateNotNullOrEmpty()] + [System.Object]$Body, + + [parameter(Mandatory = $false, ParameterSetName = "GET", HelpMessage = "Specify to use either 'Beta' or 'v1.0' API version.")] + [parameter(Mandatory = $false, ParameterSetName = "POST")] + [parameter(Mandatory = $false, ParameterSetName = "PATCH")] + [parameter(Mandatory = $false, ParameterSetName = "PUT")] + [parameter(Mandatory = $false, ParameterSetName = "DELETE")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Beta", "v1.0")] + [string]$APIVersion = "v1.0", + + [parameter(Mandatory = $false, ParameterSetName = "GET", HelpMessage = "Specify the content type for the graph request.")] + [parameter(Mandatory = $false, ParameterSetName = "POST")] + [parameter(Mandatory = $false, ParameterSetName = "PATCH")] + [parameter(Mandatory = $false, ParameterSetName = "PUT")] + [parameter(Mandatory = $false, ParameterSetName = "DELETE")] + [ValidateNotNullOrEmpty()] + [ValidateSet("application/json", "image/png")] + [string]$ContentType = "application/json" + ) + Begin { + # Construct list as return value for handling both single and multiple instances in response from call + $GraphResponseList = New-Object -TypeName "System.Collections.ArrayList" + + # Construct full URI + $GraphURI = "https://graph.microsoft.com/$($APIVersion)/$($Resource)" + Write-Verbose -Message "$($PSCmdlet.ParameterSetName) $($GraphURI)" + } + Process { + # Call Graph API and get JSON response + do { + try { + # Determine the current time in UTC + $UTCDateTime = (Get-Date).ToUniversalTime() + + # Determine the token expiration count as minutes + $TokenExpireMins = ([datetime]$Headers["ExpiresOn"] - $UTCDateTime).Minutes + + # Attempt to retrieve a refresh token when token expiration count is less than or equal to 10 + if ($TokenExpireMins -le 10) { + Write-Verbose -Message "Existing token found but has expired, requesting a new token" + $AccessToken = Get-MsalToken -TenantId $Script:TenantID -ClientId $Script:ClientID -Silent -ForceRefresh + $Headers = New-AuthenticationHeader -AccessToken $AccessToken + } + + # Construct table of default request parameters + $RequestParams = @{ + "Uri" = $GraphURI + "Headers" = $Headers + "Method" = $PSCmdlet.ParameterSetName + "ErrorAction" = "Stop" + "Verbose" = $false + } + + switch ($PSCmdlet.ParameterSetName) { + "POST" { + $RequestParams.Add("Body", $Body) + $RequestParams.Add("ContentType", $ContentType) + } + "PATCH" { + $RequestParams.Add("Body", $Body) + $RequestParams.Add("ContentType", $ContentType) + } + "PUT" { + $RequestParams.Add("Body", $Body) + $RequestParams.Add("ContentType", $ContentType) + } + } + + # Invoke Graph request + $GraphResponse = Invoke-RestMethod @RequestParams + + # Handle paging in response + if ($GraphResponse.'@odata.nextLink' -ne $null) { + $GraphResponseList.AddRange($GraphResponse.value) | Out-Null + $GraphURI = $GraphResponse.'@odata.nextLink' + Write-Verbose -Message "NextLink: $($GraphURI)" + } + else { + # NextLink from response was null, assuming last page but also handle if a single instance is returned + if (-not([string]::IsNullOrEmpty($GraphResponse.value))) { + $GraphResponseList.AddRange($GraphResponse.value) | Out-Null + } + else { + $GraphResponseList.Add($GraphResponse) | Out-Null + } + + # Set graph response as handled and stop processing loop + $GraphResponseProcess = $false + } + } + catch [System.Exception] { + $ExceptionItem = $PSItem + if ($ExceptionItem.Exception.Response.StatusCode -like "429") { + # Detected throttling based from response status code + $RetryInSeconds = $ExceptionItem.Exception.Response.Headers["Retry-After"] + + # Wait for given period of time specified in response headers + Write-Verbose -Message "Graph is throttling the request, will retry in '$($RetryInSeconds)' seconds" + Start-Sleep -Seconds $RetryInSeconds + } + else { + try { + # Read the response stream + $StreamReader = New-Object -TypeName "System.IO.StreamReader" -ArgumentList @($ExceptionItem.Exception.Response.GetResponseStream()) + $StreamReader.BaseStream.Position = 0 + $StreamReader.DiscardBufferedData() + $ResponseBody = ($StreamReader.ReadToEnd() | ConvertFrom-Json) + + switch ($PSCmdlet.ParameterSetName) { + "GET" { + # Output warning message that the request failed with error message description from response stream + Write-Warning -Message "Graph request failed with status code '$($ExceptionItem.Exception.Response.StatusCode)'. Error message: $($ResponseBody.error.message)" + + # Set graph response as handled and stop processing loop + $GraphResponseProcess = $false + } + default { + # Construct new custom error record + $SystemException = New-Object -TypeName "System.Management.Automation.RuntimeException" -ArgumentList ("{0}: {1}" -f $ResponseBody.error.code, $ResponseBody.error.message) + $ErrorRecord = New-Object -TypeName "System.Management.Automation.ErrorRecord" -ArgumentList @($SystemException, $ErrorID, [System.Management.Automation.ErrorCategory]::NotImplemented, [string]::Empty) + + # Throw a terminating custom error record + $PSCmdlet.ThrowTerminatingError($ErrorRecord) + } + } + + # Set graph response as handled and stop processing loop + $GraphResponseProcess = $false + } + catch [System.Exception] { + Write-Warning -Message "Unhandled error occurred in function. Error message: $($PSItem.Exception.Message)" + + # Set graph response as handled and stop processing loop + $GraphResponseProcess = $false + } + } + } + } + until ($GraphResponseProcess -eq $false) + + # Handle return value + return $GraphResponseList + } + } + + function New-AuthenticationHeader { + <# + .SYNOPSIS + Construct a required header hash-table based on the access token from Get-MsalToken cmdlet. + + .DESCRIPTION + Construct a required header hash-table based on the access token from Get-MsalToken cmdlet. + + .PARAMETER AccessToken + Pass the AuthenticationResult object returned from Get-MsalToken cmdlet. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-12-04 + Updated: 2020-12-04 + + Version history: + 1.0.0 - (2020-12-04) Script created + #> + param( + [parameter(Mandatory = $true, HelpMessage = "Pass the AuthenticationResult object returned from Get-MsalToken cmdlet.")] + [ValidateNotNullOrEmpty()] + [Microsoft.Identity.Client.AuthenticationResult]$AccessToken + ) + Process { + # Construct default header parameters + $AuthenticationHeader = @{ + "Content-Type" = "application/json" + "Authorization" = $AccessToken.CreateAuthorizationHeader() + "ExpiresOn" = $AccessToken.ExpiresOn.LocalDateTime + } + + # Amend header with additional required parameters for bitLocker/recoveryKeys resource query + $AuthenticationHeader.Add("ocp-client-name", "My App") + $AuthenticationHeader.Add("ocp-client-version", "1.0") + + # Handle return value + return $AuthenticationHeader + } + } + + function Get-AutopilotDevice { + <# + .SYNOPSIS + Retrieve an Autopilot device identity based on serial number. + + .DESCRIPTION + Retrieve an Autopilot device identity based on serial number. + + .PARAMETER SerialNumber + Specify the serial number of the device. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2021-01-27 + Updated: 2021-01-27 + + Version history: + 1.0.0 - (2021-01-27) Function created + #> + param( + [parameter(Mandatory = $true, HelpMessage = "Specify the serial number of the device.")] + [ValidateNotNullOrEmpty()] + [string]$SerialNumber + ) + Process { + # Retrieve the Windows Autopilot device identity by filtering on serialNumber property with passed parameter input + $SerialNumberEncoded = [Uri]::EscapeDataString($SerialNumber) + $ResourceURI = "deviceManagement/windowsAutopilotDeviceIdentities?`$filter=contains(serialNumber,'$($SerialNumberEncoded)')" + $GraphResponse = (Invoke-MSGraphOperation -Get -APIVersion "Beta" -Resource $ResourceURI -Headers $Script:AuthenticationHeader).value + + # Handle return response + return $GraphResponse + } + } + + try { + # Determine the correct RedirectUri (also known as Reply URL) to use with MSAL.PS + if ($ClientID -like "d1ddf0e4-d672-4dae-b554-9d5bdfd93547") { + $RedirectUri = "urn:ietf:wg:oauth:2.0:oob" + } + else { + $RedirectUri = [string]::Empty + } + + # Get authentication token + $AccessToken = Get-MsalToken -TenantId $TenantID -ClientId $ClientID -RedirectUri $RedirectUri -ErrorAction Stop + + # Construct authentication header + $AuthenticationHeader = New-AuthenticationHeader -AccessToken $AccessToken + + # Construct a new list to contain all device records + $DeviceList = New-Object -TypeName System.Collections.ArrayList + + try { + # Retrieve the Autopilot device identity based on serial number from parameter input + $AutopilotDevice = Get-AutopilotDevice -SerialNumber $SerialNumber -ErrorAction Stop + + try { + # Determine the hardware identifier for the associated Azure AD device record of the Autopilot device identity + $PhysicalIds = (Invoke-MSGraphOperation -Get -APIVersion "v1.0" -Resource "devices?`$filter=deviceId eq '$($AutopilotDevice.azureActiveDirectoryDeviceId)'" -Headers $AuthenticationHeader).value.physicalIds + $HardwareID = $PhysicalIds | Where-Object { $PSItem -match "^\[HWID\]:h:.*$" } + + if ($HardwareID -ne $null) { + # Retrieve all Azure AD device records matching the given hardware identifier + $DevicesResponse = (Invoke-MSGraphOperation -Get -APIVersion "v1.0" -Resource "devices?`$filter=physicalIds/any(c:c eq '$($HardwareID)')" -Headers $AuthenticationHeader) + if ($DevicesResponse.value -eq $null) { + foreach ($Response in $DevicesResponse) { + $DeviceList.Add($Response) | Out-Null + } + } + else { + $DeviceList.Add($DevicesResponse.value) | Out-Null + } + + # Handle output + foreach ($Device in $DeviceList) { + $PSObject = [PSCustomObject]@{ + DeviceName = $Device.displayName + DeviceID = $Device.deviceId + ObjectID = $Device.id + HardwareID = $HardwareID + Created = [datetime]::Parse($Device.createdDateTime) + LastSignIn = [datetime]::Parse($Device.approximateLastSignInDateTime) + TrustType = $Device.trustType + Autopilot = if ($Device.deviceId -like $AutopilotDevice.azureActiveDirectoryDeviceId) { $true } else { $false } + } + Write-Output -InputObject $PSObject + } + } + else { + "..." + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while .... Error message: $($PSItem.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while .... Error message: $($PSItem.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to retrieve an authentication token. Error message: $($PSItem.Exception.Message)" + } +} \ No newline at end of file diff --git a/Autopilot/Invoke-OneDriveSetupUpdate.ps1 b/Autopilot/Invoke-OneDriveSetupUpdate.ps1 new file mode 100644 index 0000000..63885b9 --- /dev/null +++ b/Autopilot/Invoke-OneDriveSetupUpdate.ps1 @@ -0,0 +1,332 @@ +<# +.SYNOPSIS + Download the latest OneDriveSetup.exe on the production ring, replace built-in version and initate per-machine OneDrive setup. + +.DESCRIPTION + This script will download the latest OneDriveSetup.exe from the production ring, replace the built-in executable, initiate the + per-machine install which will result in the latest version of OneDrive will always be installed and synchronization can begin right away. + +.PARAMETER DownloadPath + Specify a path for where OneDriveSetup.exe will be temporarily downloaded to. + +.EXAMPLE + .\Invoke-OneDriveSetupUpdate.ps1 + +.NOTES + FileName: Invoke-OneDriveSetupUpdate.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2021-01-18 + Updated: 2021-01-18 + + Version history: + 1.0.0 - (2021-01-18) Script created +#> +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [parameter(Mandatory = $false, HelpMessage = "Specify a path for where OneDriveSetup.exe will be temporarily downloaded to.")] + [ValidateNotNullOrEmpty()] + [string]$DownloadPath = (Join-Path -Path $env:windir -ChildPath "Temp") +) +Begin { + # Install required modules for script execution + $Modules = @("NTFSSecurity") + foreach ($Module in $Modules) { + try { + $CurrentModule = Get-InstalledModule -Name $Module -ErrorAction Stop -Verbose:$false + if ($CurrentModule -ne $null) { + $LatestModuleVersion = (Find-Module -Name $Module -ErrorAction Stop -Verbose:$false).Version + if ($LatestModuleVersion -gt $CurrentModule.Version) { + $UpdateModuleInvocation = Update-Module -Name $Module -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + } + } + catch [System.Exception] { + try { + # Install NuGet package provider + $PackageProvider = Install-PackageProvider -Name NuGet -Force -Verbose:$false + + # Install current missing module + Install-Module -Name $Module -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to install $($Module) module. Error message: $($_.Exception.Message)" + } + } + } + + # Determine the localized name of the principals required for the functionality of this script + $LocalSystemPrincipal = "NT AUTHORITY\SYSTEM" +} +Process { + # Functions + function Write-LogEntry { + param ( + [parameter(Mandatory = $true, HelpMessage = "Value added to the log file.")] + [ValidateNotNullOrEmpty()] + [string]$Value, + + [parameter(Mandatory = $true, HelpMessage = "Severity for the log entry. 1 for Informational, 2 for Warning and 3 for Error.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("1", "2", "3")] + [string]$Severity, + + [parameter(Mandatory = $false, HelpMessage = "Name of the log file that the entry will written to.")] + [ValidateNotNullOrEmpty()] + [string]$FileName = "Invoke-OneDriveSetupUpdate.log" + ) + # Determine log file location + $LogFilePath = Join-Path -Path (Join-Path -Path $env:windir -ChildPath "Temp") -ChildPath $FileName + + # Construct time stamp for log entry + $Time = -join @((Get-Date -Format "HH:mm:ss.fff"), "+", (Get-WmiObject -Class Win32_TimeZone | Select-Object -ExpandProperty Bias)) + + # Construct date for log entry + $Date = (Get-Date -Format "MM-dd-yyyy") + + # Construct context for log entry + $Context = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) + + # Construct final log entry + $LogText = "" + + # Add value to log file + try { + Out-File -InputObject $LogText -Append -NoClobber -Encoding Default -FilePath $LogFilePath -ErrorAction Stop + } + catch [System.Exception] { + Write-Warning -Message "Unable to append log entry to Invoke-OneDriveSetupUpdate.log file. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" + } + } + + function Start-DownloadFile { + param( + [parameter(Mandatory = $true, HelpMessage="URL for the file to be downloaded.")] + [ValidateNotNullOrEmpty()] + [string]$URL, + + [parameter(Mandatory = $true, HelpMessage="Folder where the file will be downloaded.")] + [ValidateNotNullOrEmpty()] + [string]$Path, + + [parameter(Mandatory = $true, HelpMessage="Name of the file including file extension.")] + [ValidateNotNullOrEmpty()] + [string]$Name + ) + Begin { + # Set global variable + $ErrorActionPreference = "Stop" + + # Construct WebClient object + $WebClient = New-Object -TypeName "System.Net.WebClient" + } + Process { + try { + # Create path if it doesn't exist + if (-not(Test-Path -Path $Path)) { + New-Item -Path $Path -ItemType Directory -Force -ErrorAction Stop | Out-Null + } + + # Start download of file + $WebClient.DownloadFile($URL, (Join-Path -Path $Path -ChildPath $Name)) + } + catch [System.Exception] { + Write-LogEntry -Value " - Failed to download file from URL '$($URL)'" -Severity 3 + } + } + End { + # Dispose of the WebClient object + $WebClient.Dispose() + } + } + + function Invoke-Executable { + param ( + [parameter(Mandatory = $true, HelpMessage = "Specify the file name or path of the executable to be invoked, including the extension.")] + [ValidateNotNullOrEmpty()] + [string]$FilePath, + + [parameter(Mandatory = $false, HelpMessage = "Specify arguments that will be passed to the executable.")] + [ValidateNotNull()] + [string]$Arguments + ) + + # Construct a hash-table for default parameter splatting + $SplatArgs = @{ + FilePath = $FilePath + NoNewWindow = $true + Passthru = $true + ErrorAction = "Stop" + } + + # Add ArgumentList param if present + if (-not ([System.String]::IsNullOrEmpty($Arguments))) { + $SplatArgs.Add("ArgumentList", $Arguments) + } + + # Invoke executable and wait for process to exit + try { + $Invocation = Start-Process @SplatArgs + $Handle = $Invocation.Handle + $Invocation.WaitForExit() + } + catch [System.Exception] { + Write-Warning -Message $_.Exception.Message; break + } + + # Handle return value with exitcode from process + return $Invocation.ExitCode + } + + try { + try { + # Attempt to remove existing OneDriveSetup.exe in temporary location + if (Test-Path -Path (Join-Path -Path $DownloadPath -ChildPath "OneDriveSetup.exe")) { + Write-LogEntry -Value "Found existing 'OneDriveSetup.exe' in temporary download path, removing it" -Severity 1 + Remove-Item -Path (Join-Path -Path $DownloadPath -ChildPath "OneDriveSetup.exe") -Force -ErrorAction Stop + } + + # Download the OneDriveSetup.exe file to temporary location + $OneDriveSetupURL = "https://go.microsoft.com/fwlink/p/?LinkId=248256" + Write-LogEntry -Value "Attempting to download the latest OneDriveSetup.exe file from Microsoft download page to temporary download path: $($DownloadPath)" -Severity 1 + Write-LogEntry -Value "Using URL for download: $($OneDriveSetupURL)" -Severity 1 + Start-DownloadFile -URL $OneDriveSetupURL -Path $DownloadPath -Name "OneDriveSetup.exe" -ErrorAction Stop + + # Validate OneDriveSetup.exe file has successfully been downloaded to temporary location + if (Test-Path -Path $DownloadPath) { + if (Test-Path -Path (Join-Path -Path $DownloadPath -ChildPath "OneDriveSetup.exe")) { + Write-LogEntry -Value "Detected 'OneDriveSetup.exe' in the temporary download path" -Severity 1 + + try { + # Attempt to import the NTFSSecurity module as a verification that it was successfully installed + Write-LogEntry -Value "Attempting to import the 'NTFSSecurity' module" -Severity 1 + Import-Module -Name "NTFSSecurity" -Verbose:$false -ErrorAction Stop + + try { + # Save the existing access rules and ownership information + Write-LogEntry -Value "Attempting to read and temporarily store existing access permissions for built-in 'OneDriveSetup.exe' executable" -Severity 1 + $OneDriveSetupFile = Join-Path -Path $env:windir -ChildPath "SysWOW64\OneDriveSetup.exe" + Write-LogEntry -Value "Reading from file: $($OneDriveSetupFile)" -Severity 1 + $OneDriveSetupAccessRules = Get-NTFSAccess -Path $OneDriveSetupFile -Verbose:$false -ErrorAction Stop + $OneDriveSetupOwner = (Get-NTFSOwner -Path $OneDriveSetupFile -ErrorAction Stop).Owner | Select-Object -ExpandProperty "AccountName" + + try { + # Set owner to system for built-in OneDriveSetup executable + Write-LogEntry -Value "Setting ownership for '$($LocalSystemPrincipal)' on file: $($OneDriveSetupFile)" -Severity 1 + Set-NTFSOwner -Path $OneDriveSetupFile -Account $LocalSystemPrincipal -ErrorAction Stop + + try { + Write-LogEntry -Value "Setting access right 'FullControl' for owner '$($LocalSystemPrincipal)' on file: '$($OneDriveSetupFile)" -Severity 1 + Add-NTFSAccess -Path $OneDriveSetupFile -Account $LocalSystemPrincipal -AccessRights "FullControl" -AccessType "Allow" -ErrorAction Stop + + try { + # Remove built-in OneDriveSetup executable + Write-LogEntry -Value "Attempting to remove built-in built-in 'OneDriveSetup.exe' executable file: $($OneDriveSetupFile)" -Severity 1 + Remove-Item -Path $OneDriveSetupFile -Force -ErrorAction Stop + + try { + # Copy downloaded OneDriveSetup file to default location + $OneDriveSetupSourceFile = Join-Path -Path $DownloadPath -ChildPath "OneDriveSetup.exe" + Write-LogEntry -Value "Attempting to copy downloaded '$($OneDriveSetupSourceFile)' to: $($OneDriveSetupFile)" -Severity 1 + Copy-Item -Path $OneDriveSetupSourceFile -Destination $OneDriveSetupFile -Force -Verbose:$false -ErrorAction Stop + + try { + # Restore access rules and owner information + foreach ($OneDriveSetupAccessRule in $OneDriveSetupAccessRules) { + if ($OneDriveSetupAccessRule.Account.AccountName -match "APPLICATION PACKAGE AUTHORITY") { + $AccountName = ($OneDriveSetupAccessRule.Account.AccountName.Split("\"))[1] + } + else { + $AccountName = $OneDriveSetupAccessRule.Account.AccountName + } + + Write-LogEntry -Value "Restoring access right '$($OneDriveSetupAccessRule.AccessRights)' for account '$($AccountName)' on file: $($OneDriveSetupFile)" -Severity 1 + Add-NTFSAccess -Path $OneDriveSetupFile -Account $AccountName -AccessRights $OneDriveSetupAccessRule.AccessRights -AccessType "Allow" -ErrorAction Stop + } + + try { + # Disable inheritance for the updated built-in OneDriveSetup executable + Write-LogEntry -Value "Disabling and removing inherited access rules on file: $($OneDriveSetupFile)" -Severity 1 + Disable-NTFSAccessInheritance -Path $OneDriveSetupFile -RemoveInheritedAccessRules -ErrorAction Stop + + try { + # Restore owner information + Write-LogEntry -Value "Restoring owner '$($OneDriveSetupOwner)' on file: $($OneDriveSetupFile)" -Severity 1 + Set-NTFSOwner -Path $OneDriveSetupFile -Account $OneDriveSetupOwner -ErrorAction Stop + + try { + # Attempt to remove existing OneDriveSetup.exe in temporary location + if (Test-Path -Path $OneDriveSetupSourceFile) { + Write-LogEntry -Value "Deleting 'OneDriveSetup.exe' from temporary download path" -Severity 1 + Remove-Item -Path $OneDriveSetupSourceFile -Force -ErrorAction Stop + } + + Write-LogEntry -Value "Successfully updated built-in 'OneDriveSetup.exe' executable to the latest version" -Severity 1 + + try { + # Initiate updated built-in OneDriveSetup.exe and install as per-machine + Write-LogEntry -Value "Initiate per-machine OneDrive setup installation, this process could take some time" -Severity 1 + Invoke-Executable -FilePath $OneDriveSetupFile -Arguments "/allusers /update" -ErrorAction Stop + + Write-LogEntry -Value "Successfully installed OneDrive as per-machine" -Severity 1 + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to install OneDrive as per-machine. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to remove '$($OneDriveSetupSourceFile)'. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to restore owner for account '$($OneDriveSetupOwner)'. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to disable inheritance for '$($OneDriveSetupFile)'. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to restore access right '$($OneDriveSetupAccessRule.AccessRights)' for account '$($OneDriveSetupAccessRule.Account.AccountName)' on file '$($OneDriveSetupFile)'. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to copy '$($OneDriveSetupSourceFile)' to default location. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to remove built-in executable file '$($OneDriveSetupFile)'. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to set access right 'FullControl' for owner on file: '$($OneDriveSetupFile)'. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to set ownership for '$($LocalSystemPrincipal)' on file: $($OneDriveSetupFile). Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to temporarily store existing access permissions for built-in 'OneDriveSetup.exe' executable. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to import the 'NTFSSecurity' module. Error message: $($_.Exception.Message)" -Severity 3 + } + } + else { + Write-LogEntry -Value "Unable to detect 'OneDriveSetup.exe' in the temporary download path" -Severity 3 + } + } + else { + Write-LogEntry -Value "Unable to locate download path '$($DownloadPath)', ensure the directory exists" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to restore owner for account '$($OneDriveSetupOwner)'. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to download OneDriveSetup.exe file. Error message: $($_.Exception.Message)" -Severity 3 + } +} \ No newline at end of file diff --git a/Autopilot/Set-AutopilotDeviceGroupTag.ps1 b/Autopilot/Set-AutopilotDeviceGroupTag.ps1 new file mode 100644 index 0000000..0b164f2 --- /dev/null +++ b/Autopilot/Set-AutopilotDeviceGroupTag.ps1 @@ -0,0 +1,493 @@ +<# +.SYNOPSIS + Set the Group Tag of an explicit Autopilot device or an array of devices to a specific value. + +.DESCRIPTION + This script will set the Group Tag of an explicit Autopilot device or an array of devices. The serial number + of a device, or multiple, are used as the device idenfier in the Autopilot service. All devices will get the + same static Group Tag value, used as input for the Value parameter. + +.PARAMETER TenantID + Specify the Azure AD tenant ID or the common name, e.g. 'tenant.onmicrosoft.com'. + +.PARAMETER ClientID + Specify the service principal (also known as an app registration) Client ID (also known as Application ID). + +.PARAMETER SerialNumber + Specify an explicit or an array of serial numbers, to be used as the identifier when querying the Autopilot service for devices. + +.PARAMETER Value + Specify the Group Tag value to be set for all identified devices. + +.EXAMPLE + # Update the Group Tag of a device with serial number '1234567', with a value of 'GroupTag1': + .\Set-AutopilotDeviceGroupTag.ps1 -TenantID "tenant.onmicrosoft.com" -ClientID "" -SerialNumber "1234567" -Value "GroupTag1" + + # Update the Group Tag of a multiple devices in an array, with a value of 'GroupTag1': + .\Set-AutopilotDeviceGroupTag.ps1 -TenantID "tenant.onmicrosoft.com" -ClientID "" -SerialNumber @("1234567", "2345678") -Value "GroupTag1" + +.NOTES + FileName: Set-AutopilotDeviceGroupTag.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2021-02-21 + Updated: 2021-02-21 + + Version history: + 1.0.0 - (2021-02-21) Script created +#> +#Requires -Modules "MSAL.PS" +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [parameter(Mandatory = $true, HelpMessage = "Specify the Azure AD tenant ID or the common name, e.g. 'tenant.onmicrosoft.com'.")] + [ValidateNotNullOrEmpty()] + [string]$TenantID, + + [parameter(Mandatory = $true, HelpMessage = "Specify the service principal (also known as an app registration) Client ID (also known as Application ID).")] + [ValidateNotNullOrEmpty()] + [string]$ClientID, + + [parameter(Mandatory = $true, HelpMessage = "Specify an explicit or an array of serial numbers, to be used as the identifier when querying the Autopilot service for devices.")] + [ValidateNotNullOrEmpty()] + [string[]]$SerialNumber, + + [parameter(Mandatory = $true, HelpMessage = "Specify the Group Tag value to be set for all identified devices.")] + [ValidateNotNullOrEmpty()] + [string]$Value +) +Begin {} +Process { + # Functions + function New-AuthenticationHeader { + <# + .SYNOPSIS + Construct a required header hash-table based on the access token from Get-MsalToken cmdlet. + + .DESCRIPTION + Construct a required header hash-table based on the access token from Get-MsalToken cmdlet. + + .PARAMETER AccessToken + Pass the AuthenticationResult object returned from Get-MsalToken cmdlet. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-12-04 + Updated: 2020-12-04 + + Version history: + 1.0.0 - (2020-12-04) Script created + #> + param( + [parameter(Mandatory = $true, HelpMessage = "Pass the AuthenticationResult object returned from Get-MsalToken cmdlet.")] + [ValidateNotNullOrEmpty()] + [Microsoft.Identity.Client.AuthenticationResult]$AccessToken + ) + Process { + # Construct default header parameters + $AuthenticationHeader = @{ + "Content-Type" = "application/json" + "Authorization" = $AccessToken.CreateAuthorizationHeader() + "ExpiresOn" = $AccessToken.ExpiresOn.LocalDateTime + } + + # Amend header with additional required parameters for bitLocker/recoveryKeys resource query + $AuthenticationHeader.Add("ocp-client-name", "My App") + $AuthenticationHeader.Add("ocp-client-version", "1.0") + + # Handle return value + return $AuthenticationHeader + } + } + + function Invoke-MSGraphOperation { + <# + .SYNOPSIS + Perform a specific call to Intune Graph API, either as GET, POST, PATCH or DELETE methods. + + .DESCRIPTION + Perform a specific call to Intune Graph API, either as GET, POST, PATCH or DELETE methods. + This function handles nextLink objects including throttling based on retry-after value from Graph response. + + .PARAMETER Get + Switch parameter used to specify the method operation as 'GET'. + + .PARAMETER Post + Switch parameter used to specify the method operation as 'POST'. + + .PARAMETER Patch + Switch parameter used to specify the method operation as 'PATCH'. + + .PARAMETER Put + Switch parameter used to specify the method operation as 'PUT'. + + .PARAMETER Delete + Switch parameter used to specify the method operation as 'DELETE'. + + .PARAMETER Resource + Specify the full resource path, e.g. deviceManagement/auditEvents. + + .PARAMETER Headers + Specify a hash-table as the header containing minimum the authentication token. + + .PARAMETER Body + Specify the body construct. + + .PARAMETER APIVersion + Specify to use either 'Beta' or 'v1.0' API version. + + .PARAMETER ContentType + Specify the content type for the graph request. + + .NOTES + Author: Nickolaj Andersen & Jan Ketil Skanke + Contact: @JankeSkanke @NickolajA + Created: 2020-10-11 + Updated: 2020-11-11 + + Version history: + 1.0.0 - (2020-10-11) Function created + 1.0.1 - (2020-11-11) Verified + #> + param( + [parameter(Mandatory = $true, ParameterSetName = "GET", HelpMessage = "Switch parameter used to specify the method operation as 'GET'.")] + [switch]$Get, + + [parameter(Mandatory = $true, ParameterSetName = "POST", HelpMessage = "Switch parameter used to specify the method operation as 'POST'.")] + [switch]$Post, + + [parameter(Mandatory = $true, ParameterSetName = "PATCH", HelpMessage = "Switch parameter used to specify the method operation as 'PATCH'.")] + [switch]$Patch, + + [parameter(Mandatory = $true, ParameterSetName = "PUT", HelpMessage = "Switch parameter used to specify the method operation as 'PUT'.")] + [switch]$Put, + + [parameter(Mandatory = $true, ParameterSetName = "DELETE", HelpMessage = "Switch parameter used to specify the method operation as 'DELETE'.")] + [switch]$Delete, + + [parameter(Mandatory = $true, ParameterSetName = "GET", HelpMessage = "Specify the full resource path, e.g. deviceManagement/auditEvents.")] + [parameter(Mandatory = $true, ParameterSetName = "POST")] + [parameter(Mandatory = $true, ParameterSetName = "PATCH")] + [parameter(Mandatory = $true, ParameterSetName = "PUT")] + [parameter(Mandatory = $true, ParameterSetName = "DELETE")] + [ValidateNotNullOrEmpty()] + [string]$Resource, + + [parameter(Mandatory = $true, ParameterSetName = "GET", HelpMessage = "Specify a hash-table as the header containing minimum the authentication token.")] + [parameter(Mandatory = $true, ParameterSetName = "POST")] + [parameter(Mandatory = $true, ParameterSetName = "PATCH")] + [parameter(Mandatory = $true, ParameterSetName = "PUT")] + [parameter(Mandatory = $true, ParameterSetName = "DELETE")] + [ValidateNotNullOrEmpty()] + [System.Collections.Hashtable]$Headers, + + [parameter(Mandatory = $false, ParameterSetName = "POST", HelpMessage = "Specify the body construct.")] + [parameter(Mandatory = $true, ParameterSetName = "PATCH")] + [parameter(Mandatory = $true, ParameterSetName = "PUT")] + [ValidateNotNullOrEmpty()] + [System.Object]$Body, + + [parameter(Mandatory = $false, ParameterSetName = "GET", HelpMessage = "Specify to use either 'Beta' or 'v1.0' API version.")] + [parameter(Mandatory = $false, ParameterSetName = "POST")] + [parameter(Mandatory = $false, ParameterSetName = "PATCH")] + [parameter(Mandatory = $false, ParameterSetName = "PUT")] + [parameter(Mandatory = $false, ParameterSetName = "DELETE")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Beta", "v1.0")] + [string]$APIVersion = "v1.0", + + [parameter(Mandatory = $false, ParameterSetName = "GET", HelpMessage = "Specify the content type for the graph request.")] + [parameter(Mandatory = $false, ParameterSetName = "POST")] + [parameter(Mandatory = $false, ParameterSetName = "PATCH")] + [parameter(Mandatory = $false, ParameterSetName = "PUT")] + [parameter(Mandatory = $false, ParameterSetName = "DELETE")] + [ValidateNotNullOrEmpty()] + [ValidateSet("application/json", "image/png")] + [string]$ContentType = "application/json" + ) + Begin { + # Construct list as return value for handling both single and multiple instances in response from call + $GraphResponseList = New-Object -TypeName "System.Collections.ArrayList" + + # Construct full URI + $GraphURI = "https://graph.microsoft.com/$($APIVersion)/$($Resource)" + Write-Verbose -Message "$($PSCmdlet.ParameterSetName) $($GraphURI)" + } + Process { + # Call Graph API and get JSON response + do { + try { + # Construct table of default request parameters + $RequestParams = @{ + "Uri" = $GraphURI + "Headers" = $Headers + "Method" = $PSCmdlet.ParameterSetName + "ErrorAction" = "Stop" + "Verbose" = $false + } + + switch ($PSCmdlet.ParameterSetName) { + "POST" { + if ($PSBoundParameters["Body"]) { + $RequestParams.Add("Body", $Body) + } + if (-not([string]::IsNullOrEmpty($ContentType))) { + $RequestParams.Add("ContentType", $ContentType) + } + } + "PATCH" { + $RequestParams.Add("Body", $Body) + $RequestParams.Add("ContentType", $ContentType) + } + "PUT" { + $RequestParams.Add("Body", $Body) + $RequestParams.Add("ContentType", $ContentType) + } + } + + # Invoke Graph request + $GraphResponse = Invoke-RestMethod @RequestParams + + # Handle paging in response + if ($GraphResponse.'@odata.nextLink' -ne $null) { + $GraphResponseList.AddRange($GraphResponse.value) | Out-Null + $GraphURI = $GraphResponse.'@odata.nextLink' + Write-Verbose -Message "NextLink: $($GraphURI)" + } + else { + # NextLink from response was null, assuming last page but also handle if a single instance is returned + if (-not([string]::IsNullOrEmpty($GraphResponse.value))) { + $GraphResponseList.AddRange($GraphResponse.value) | Out-Null + } + else { + $GraphResponseList.Add($GraphResponse) | Out-Null + } + + # Set graph response as handled and stop processing loop + $GraphResponseProcess = $false + } + } + catch [System.Exception] { + # Capture current error + $ExceptionItem = $PSItem + + # Read the response stream + $StreamReader = New-Object -TypeName "System.IO.StreamReader" -ArgumentList @($ExceptionItem.Exception.Response.GetResponseStream()) -ErrorAction SilentlyContinue + if ($StreamReader -ne $null) { + $StreamReader.BaseStream.Position = 0 + $StreamReader.DiscardBufferedData() + $ResponseBody = ($StreamReader.ReadToEnd() | ConvertFrom-Json) + + if ($ExceptionItem.Exception.Response.StatusCode -like "429") { + # Detected throttling based from response status code + $RetryInSeconds = $ExceptionItem.Exception.Response.Headers["Retry-After"] + + if ($RetryInSeconds -ne $null) { + # Wait for given period of time specified in response headers + Write-Verbose -Message "Graph is throttling the request, will retry in '$($RetryInSeconds)' seconds" + Start-Sleep -Seconds $RetryInSeconds + } + else { + Write-Verbose -Message "Graph is throttling the request, will retry in default '300' seconds" + Start-Sleep -Seconds 300 + } + } + else { + switch ($PSCmdlet.ParameterSetName) { + "GET" { + # Output warning message that the request failed with error message description from response stream + Write-Warning -Message "Graph request failed with status code '$($ExceptionItem.Exception.Response.StatusCode)'. Error message: $($ResponseBody.error.message)" + + # Set graph response as handled and stop processing loop + $GraphResponseProcess = $false + } + default { + # Construct new custom error record + $SystemException = New-Object -TypeName "System.Management.Automation.RuntimeException" -ArgumentList ("{0}: {1}" -f $ResponseBody.error.code, $ResponseBody.error.message) + $ErrorRecord = New-Object -TypeName "System.Management.Automation.ErrorRecord" -ArgumentList @($SystemException, $ErrorID, [System.Management.Automation.ErrorCategory]::NotImplemented, [string]::Empty) + + # Throw a terminating custom error record + $PSCmdlet.ThrowTerminatingError($ErrorRecord) + } + } + + # Set graph response as handled and stop processing loop + $GraphResponseProcess = $false + } + } + else { + Write-Warning -Message "Failed with message: $($ExceptionItem.Exception.Message)" + + # Set graph response as handled and stop processing loop + $GraphResponseProcess = $false + } + } + } + until ($GraphResponseProcess -eq $false) + + # Handle return value + return $GraphResponseList + } + } + + function Get-AutopilotDevice { + <# + .SYNOPSIS + Retrieve an Autopilot device identity based on serial number. + + .DESCRIPTION + Retrieve an Autopilot device identity based on serial number. + + .PARAMETER SerialNumber + Specify the serial number of the device. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2021-02-21 + Updated: 2021-02-21 + + Version history: + 1.0.0 - (2021-02-21) Function created + #> + param( + [parameter(Mandatory = $true, HelpMessage = "Specify the serial number of the device.")] + [ValidateNotNullOrEmpty()] + [string]$SerialNumber + ) + Process { + # Retrieve the Windows Autopilot device identity by filtering on serialNumber property with passed parameter input + $SerialNumberEncoded = [Uri]::EscapeDataString($SerialNumber) + $ResourceURI = "deviceManagement/windowsAutopilotDeviceIdentities?`$filter=contains(serialNumber,'$($SerialNumberEncoded)')" + $GraphResponse = (Invoke-MSGraphOperation -Get -APIVersion "Beta" -Resource $ResourceURI -Headers $Script:AuthenticationHeader).value + + # Handle return response + return $GraphResponse + } + } + + function Set-AutopilotDevice { + <# + .SYNOPSIS + Update the GroupTag for an Autopilot device identity. + + .DESCRIPTION + Update the GroupTag for an Autopilot device identity. + + .PARAMETER Id + Specify the Autopilot device identity id. + + .PARAMETER GroupTag + Specify the Group Tag string value. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2021-02-21 + Updated: 2021-02-21 + + Version history: + 1.0.0 - (2021-02-21) Function created + #> + param( + [parameter(Mandatory = $true, HelpMessage = "Specify the Autopilot device identity id.")] + [ValidateNotNullOrEmpty()] + [string]$Id, + + [parameter(Mandatory = $true, HelpMessage = "Specify the Group Tag string value.")] + [ValidateNotNullOrEmpty()] + [string]$GroupTag + ) + Process { + # Construct JSON post body content + $BodyTable = @{ + "groupTag" = $GroupTag + } + $BodyJSON = ConvertTo-Json -InputObject $BodyTable + + # Update Autopilot device properties with new group tag string + $ResourceURI = "deviceManagement/windowsAutopilotDeviceIdentities/$($Id)/UpdateDeviceProperties" + $GraphResponse = Invoke-MSGraphOperation -Post -APIVersion "Beta" -Resource $ResourceURI -Headers $Script:AuthenticationHeader -Body $BodyJSON -ContentType "application/json" + + # Handle return response + return $GraphResponse + } + } + + try { + # Determine the correct RedirectUri (also known as Reply URL) to use with MSAL.PS + if ($ClientID -like "d1ddf0e4-d672-4dae-b554-9d5bdfd93547") { + $RedirectUri = "urn:ietf:wg:oauth:2.0:oob" + } + else { + $RedirectUri = [string]::Empty + } + + # Get authentication token + $AccessToken = Get-MsalToken -TenantId $TenantID -ClientId $ClientID -RedirectUri $RedirectUri -ErrorAction Stop + + try { + # Construct authentication header + $AuthenticationHeader = New-AuthenticationHeader -AccessToken $AccessToken -ErrorAction Stop + + try { + # Construct list to hold all Autopilot device objects + $AutopilotDevices = New-Object -TypeName "System.Collections.ArrayList" + + # Retrieve list of Autopilot devices based on parameter input from SerialNumber + foreach ($SerialNumberItem in $SerialNumber) { + Write-Verbose -Message "Attempting to get Autopilot device with serial number: $($SerialNumberItem)" + $AutopilotDevice = Get-AutopilotDevice -SerialNumber $SerialNumberItem -ErrorAction Stop + if ($AutopilotDevice -ne $null) { + $AutopilotDevices.Add($AutopilotDevice) | Out-Null + } + else { + Write-Warning -Message "Unable to get Autopilot device with serial number: $($SerialNumberItem)" + } + } + + # Set group tag for all identified Autopilot devices + if ($AutopilotDevices.Count -ge 1) { + if ($PSCmdlet.ShouldProcess("$($AutopilotDevices.Count) Autopilot devices", "Set Group Tag")) { + foreach ($AutopilotDevice in $AutopilotDevices) { + try { + # Set group tag for current Autopilot device + Write-Verbose -Message "Setting Group Tag value '$($Value)' for Autopilot device: $($AutopilotDevice.serialNumber)" + Set-AutopilotDevice -Id $AutopilotDevice.id -GroupTag $Value -ErrorAction Stop + + # Handle success output + $PSObject = [PSCustomObject]@{ + SerialNumber = $AutopilotDevice.serialNumber + GroupTag = $Value + Result = "Success" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while setting the Group Tag for Autopilot device with serial number '$($AutopilotDevices.serialNumber)'. Error message: $($PSItem.Exception.Message)" + + # Handle failure output + $PSObject = [PSCustomObject]@{ + SerialNumber = $AutopilotDevice.serialNumber + GroupTag = $Value + Result = "Success" + } + } + + # Handle current item output return + Write-Output -InputObject $PSObject + } + } + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while retrieving all Autopilot devices matching serial number input. Error message: $($PSItem.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while constructing the authentication header. Error message: $($PSItem.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to retrieve an authentication token. Error message: $($PSItem.Exception.Message)" + } +} diff --git a/Autopilot/Set-WindowsTimeZone.ps1 b/Autopilot/Set-WindowsTimeZone.ps1 new file mode 100644 index 0000000..5c42c97 --- /dev/null +++ b/Autopilot/Set-WindowsTimeZone.ps1 @@ -0,0 +1,291 @@ +<# +.SYNOPSIS + Automatically detect the current location using Location Services in Windows 10 and call the Azure Maps API to determine and set the Windows time zone based on current location data. + +.DESCRIPTION + This script will automatically set the Windows time zone based on current location data. It does so by detecting the current position (latitude and longitude) from Location services + in Windows 10 and then calls the Azure Maps API to determine correct Windows time zone based of the current position. If Location Services is not enabled in Windows 10, it will automatically + be enabled and ensuring the service is running. + +.PARAMETER AzureMapsSharedKey + Specify the Azure Maps API shared key available under the Authentication blade of the resource in Azure. + +.NOTES + FileName: Set-WindowsTimeZone.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-05-19 + Updated: 2020-12-22 + + Version history: + 1.0.0 - (2020-05-19) - Script created + 1.0.1 - (2020-05-23) - Added registry key presence check for lfsvc configuration and better handling of selecting a single Windows time zone when multiple objects with different territories where returned (thanks to @jgkps for reporting) + 1.0.2 - (2020-09-10) - Improved registry key handling for enabling location services + 1.0.3 - (2020-12-22) - Added support for TLS 1.2 to disable location services once script has completed +#> +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [parameter(Mandatory = $false, HelpMessage = "Specify the Azure Maps API shared key available under the Authentication blade of the resource in Azure.")] + [ValidateNotNullOrEmpty()] + [string]$AzureMapsSharedKey = "" +) +Begin { + # Enable TLS 1.2 support for downloading modules from PSGallery + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +} +Process { + # Functions + function Write-LogEntry { + param ( + [parameter(Mandatory = $true, HelpMessage = "Value added to the log file.")] + [ValidateNotNullOrEmpty()] + [string]$Value, + + [parameter(Mandatory = $true, HelpMessage = "Severity for the log entry. 1 for Informational, 2 for Warning and 3 for Error.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("1", "2", "3")] + [string]$Severity + ) + # Determine log file location + $LogFilePath = Join-Path -Path (Join-Path -Path $env:windir -ChildPath "Temp") -ChildPath "Set-WindowsTimeZone.log" + + # Construct time stamp for log entry + if (-not(Test-Path -Path 'variable:global:TimezoneBias')) { + [string]$global:TimezoneBias = [System.TimeZoneInfo]::Local.GetUtcOffset((Get-Date)).TotalMinutes + if ($TimezoneBias -match "^-") { + $TimezoneBias = $TimezoneBias.Replace('-', '+') + } + else { + $TimezoneBias = '-' + $TimezoneBias + } + } + $Time = -join @((Get-Date -Format "HH:mm:ss.fff"), $TimezoneBias) + + # Construct date for log entry + $Date = (Get-Date -Format "MM-dd-yyyy") + + # Construct context for log entry + $Context = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) + + # Construct final log entry + $LogText = "" + + # Add value to log file + try { + Out-File -InputObject $LogText -Append -NoClobber -Encoding Default -FilePath $LogFilePath -ErrorAction Stop + } + catch [System.Exception] { + Write-Warning -Message "Unable to append log entry to Set-WindowsTimeZone.log file. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" + } + } + + function Get-GeoCoordinate { + # Construct return value object + $Coordinates = [PSCustomObject]@{ + Latitude = $null + Longitude = $null + } + + Write-LogEntry -Value "Attempting to start resolving the current device coordinates" -Severity 1 + $GeoCoordinateWatcher = New-Object -TypeName "System.Device.Location.GeoCoordinateWatcher" + $GeoCoordinateWatcher.Start() + + # Wait until watcher resolves current location coordinates + $GeoCounter = 0 + while (($GeoCoordinateWatcher.Status -notlike "Ready") -and ($GeoCoordinateWatcher.Permission -notlike "Denied") -and ($GeoCounter -le 60)) { + Start-Sleep -Seconds 1 + $GeoCounter++ + } + + # Break operation and return empty object since permission was denied + if ($GeoCoordinateWatcher.Permission -like "Denied") { + Write-LogEntry -Value "Permission was denied accessing coordinates from location services" -Severity 3 + + # Stop and dispose of the GeCoordinateWatcher object + $GeoCoordinateWatcher.Stop() + $GeoCoordinateWatcher.Dispose() + + # Handle return error + return $Coordinates + } + + # Set coordinates for return value + $Coordinates.Latitude = ($GeoCoordinateWatcher.Position.Location.Latitude).ToString().Replace(",", ".") + $Coordinates.Longitude = ($GeoCoordinateWatcher.Position.Location.Longitude).ToString().Replace(",", ".") + + # Stop and dispose of the GeCoordinateWatcher object + $GeoCoordinateWatcher.Stop() + $GeoCoordinateWatcher.Dispose() + + # Handle return value + return $Coordinates + } + + function New-RegistryKey { + param( + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Path + ) + try { + Write-LogEntry -Value "Checking presence of registry key: $($Path)" -Severity 1 + if (-not(Test-Path -Path $Path)) { + Write-LogEntry -Value "Attempting to create registry key: $($Path)" -Severity 1 + New-Item -Path $Path -ItemType "Directory" -Force -ErrorAction Stop | Out-Null + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to create registry key '$($Path)'. Error message: $($_.Exception.Message)" -Severity 3 + } + } + + function Set-RegistryValue { + param( + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Path, + + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Value, + + [parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [ValidateSet("String", "ExpandString", "Binary", "DWord", "MultiString", "Qword")] + [string]$Type = "String" + ) + try { + Write-LogEntry -Value "Checking presence of registry value '$($Name)' in registry key: $($Path)" -Severity 1 + $RegistryValue = Get-ItemPropertyValue -Path $Path -Name $Name -ErrorAction SilentlyContinue + if ($RegistryValue -ne $null) { + Write-LogEntry -Value "Setting registry value '$($Name)' to: $($Value)" -Severity 1 + Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -ErrorAction Stop + } + else { + New-RegistryKey -Path $Path -ErrorAction Stop + Write-LogEntry -Value "Setting registry value '$($Name)' to: $($Value)" -Severity 1 + New-ItemProperty -Path $Path -Name $Name -PropertyType $Type -Value $Value -Force -ErrorAction Stop | Out-Null + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to create or update registry value '$($Name)' in '$($Path)'. Error message: $($_.Exception.Message)" -Severity 3 + } + } + + function Enable-LocationServices { + $AppsAccessLocation = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy" + Set-RegistryValue -Path $AppsAccessLocation -Name "LetAppsAccessLocation" -Value 0 -Type "DWord" + + $LocationConsentKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\location" + Set-RegistryValue -Path $LocationConsentKey -Name "Value" -Value "Allow" -Type "String" + + $SensorPermissionStateKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Sensor\Overrides\{BFA794E4-F964-4FDB-90F6-51056BFE4B44}" + Set-RegistryValue -Path $SensorPermissionStateKey -Name "SensorPermissionState" -Value 1 -Type "DWord" + + $LocationServiceConfigurationKey = "HKLM:\SYSTEM\CurrentControlSet\Services\lfsvc\Service\Configuration" + Set-RegistryValue -Path $LocationServiceConfigurationKey -Name "Status" -Value 1 -Type "DWord" + + $LocationService = Get-Service -Name "lfsvc" + Write-LogEntry -Value "Checking location service 'lfsvc' for status: Running" -Severity 1 + if ($LocationService.Status -notlike "Running") { + Write-LogEntry -Value "Location service is not running, attempting to start service" -Severity 1 + Start-Service -Name "lfsvc" + } + } + + function Disable-LocationServices { + $LocationConsentKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\location" + Set-RegistryValue -Path $LocationConsentKey -Name "Value" -Value "Deny" -Type "String" + + $SensorPermissionStateKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Sensor\Overrides\{BFA794E4-F964-4FDB-90F6-51056BFE4B44}" + Set-RegistryValue -Path $SensorPermissionStateKey -Name "SensorPermissionState" -Value 0 -Type "DWord" + + $LocationServiceConfigurationKey = "HKLM:\SYSTEM\CurrentControlSet\Services\lfsvc\Service\Configuration" + Set-RegistryValue -Path $LocationServiceConfigurationKey -Name "Status" -Value 0 -Type "DWord" + } + + Write-LogEntry -Value "Starting to determine the desired Windows time zone configuration" -Severity 1 + + try { + # Load required assembly and construct a GeCoordinateWatcher object + Write-LogEntry -Value "Attempting to load required 'System.Device' assembly" -Severity 1 + Add-Type -AssemblyName "System.Device" -ErrorAction Stop + + try { + # Ensure Location Services in Windows is enabled and service is running + Enable-LocationServices + + # Retrieve the latitude and longitude values + $GeoCoordinates = Get-GeoCoordinate + if (($GeoCoordinates.Latitude -ne $null) -and ($GeoCoordinates.Longitude -ne $null)) { + Write-LogEntry -Value "Successfully resolved current device coordinates" -Severity 1 + Write-LogEntry -Value "Detected latitude: $($GeoCoordinates.Latitude)" -Severity 1 + Write-LogEntry -Value "Detected longitude: $($GeoCoordinates.Longitude)" -Severity 1 + + # Construct query string for Azure Maps API request + $AzureMapsQuery = -join@($GeoCoordinates.Latitude, ",", $GeoCoordinates.Longitude) + + try { + # Call Azure Maps timezone/byCoordinates API to retrieve IANA time zone id + Write-LogEntry -Value "Attempting to determine IANA time zone id from Azure MAPS API using query: $($AzureMapsQuery)" -Severity 1 + $AzureMapsTimeZoneURI = "https://atlas.microsoft.com/timezone/byCoordinates/json?subscription-key=$($AzureMapsSharedKey)&api-version=1.0&options=all&query=$($AzureMapsQuery)" + $AzureMapsTimeZoneResponse = Invoke-RestMethod -Uri $AzureMapsTimeZoneURI -Method "Get" -ErrorAction Stop + if ($AzureMapsTimeZoneResponse -ne $null) { + $IANATimeZoneValue = $AzureMapsTimeZoneResponse.TimeZones.Id + Write-LogEntry -Value "Successfully retrieved IANA time zone id from current position data: $($IANATimeZoneValue)" -Severity 1 + + try { + # Call Azure Maps timezone/enumWindows API to retrieve the Windows time zone id + Write-LogEntry -Value "Attempting to Azure Maps API to enumerate Windows time zone ids" -Severity 1 + $AzureMapsWindowsEnumURI = "https://atlas.microsoft.com/timezone/enumWindows/json?subscription-key=$($AzureMapsSharedKey)&api-version=1.0" + $AzureMapsWindowsEnumResponse = Invoke-RestMethod -Uri $AzureMapsWindowsEnumURI -Method "Get" -ErrorAction Stop + if ($AzureMapsWindowsEnumResponse -ne $null) { + $TimeZoneID = $AzureMapsWindowsEnumResponse | Where-Object { ($PSItem.IanaIds -like $IANATimeZoneValue) -and ($PSItem.Territory.Length -eq 2) } | Select-Object -ExpandProperty WindowsId + Write-LogEntry -Value "Successfully determined the Windows time zone id: $($TimeZoneID)" -Severity 1 + + try { + # Set the time zone + Write-LogEntry -Value "Attempting to configure the Windows time zone id with value: $($TimeZoneID)" -Severity 1 + Set-TimeZone -Id $TimeZoneID -ErrorAction Stop + Write-LogEntry -Value "Successfully configured the Windows time zone" -Severity 1 + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to set Windows time zone. Error message: $($PSItem.Exception.Message)" -Severity 3 + } + } + else { + Write-LogEntry -Value "Invalid response from Azure Maps call enumerating Windows time zone ids" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to call Azure Maps API to enumerate Windows time zone ids. Error message: $($PSItem.Exception.Message)" -Severity 3 + } + } + else { + Write-LogEntry -Value "Invalid response from Azure Maps query when attempting to retrieve the IANA time zone id" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to retrieve the IANA time zone id based on current position data from Azure Maps. Error message: $($PSItem.Exception.Message)" -Severity 3 + } + } + else { + Write-LogEntry -Value "Unable to determine current device coordinates from location services, breaking operation" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to determine Windows time zone. Error message: $($PSItem.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to load required 'System.Device' assembly, breaking operation" -Severity 3 + } +} +End { + # Set Location Services to disabled to let other policy configuration manage the state + Disable-LocationServices +} \ No newline at end of file diff --git a/Autopilot/Test-AutopilotAzureADDeviceAssociation.ps1 b/Autopilot/Test-AutopilotAzureADDeviceAssociation.ps1 new file mode 100644 index 0000000..e301e19 --- /dev/null +++ b/Autopilot/Test-AutopilotAzureADDeviceAssociation.ps1 @@ -0,0 +1,177 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID c30dcb72-e391-49ae-ad06-e5438b8c72a1 +.AUTHOR NickolajA +.DESCRIPTION Validate that the configured Azure AD device record for all Autopilot device identities exist in Azure AD. +.COMPANYNAME MSEndpointMgr +.COPYRIGHT +.TAGS AzureAD Autopilot Windows Intune +.LICENSEURI +.PROJECTURI https://github.com/MSEndpointMgr/Intune/blob/master/Autopilot/Test-AutopilotAzureADDeviceAssociation.ps1 +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES +#> +#Requires -Module MSGraphRequest +#Requires -Module MSAL.PS +<# +.SYNOPSIS + Validate that the configured Azure AD device record for all Autopilot device identities exist in Azure AD. + +.DESCRIPTION + This script will retrieve all Autopilot identities and foreach validate if the given Azure AD device record that's + currently associated actually exist in Azure AD. + +.PARAMETER TenantID + Specify the tenant name or ID, e.g. tenant.onmicrosoft.com or . + +.EXAMPLE + .\Test-AutopilotAzureADDeviceAssociation.ps1 -TenantID "tenantname.onmicrosoft.com" + +.NOTES + FileName: Test-AutopilotAzureADDeviceAssociation.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2021-05-06 + Updated: 2021-05-06 + + Version history: + 1.0.0 - (2021-05-06) Script created +#> +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [parameter(Mandatory = $true, HelpMessage = "Specify the tenant name or ID, e.g. tenant.onmicrosoft.com or .")] + [ValidateNotNullOrEmpty()] + [string]$TenantID +) +Process { + # Functions + function Get-AutopilotDevice { + <# + .SYNOPSIS + Retrieve all Autopilot device identities. + + .DESCRIPTION + Retrieve all Autopilot device identities. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2021-01-27 + Updated: 2021-01-27 + + Version history: + 1.0.0 - (2021-01-27) Function created + #> + Process { + # Retrieve all Windows Autopilot device identities + $ResourceURI = "deviceManagement/windowsAutopilotDeviceIdentities" + $GraphResponse = Invoke-MSGraphOperation -Get -APIVersion "Beta" -Resource $ResourceURI + + # Handle return response + return $GraphResponse + } + } + + function Get-AzureADDeviceRecord { + <# + .SYNOPSIS + Retrieve an Azure AD device record. + + .DESCRIPTION + Retrieve an Azure AD device record. + + .PARAMETER DeviceId + Specify the Device ID of the Azure AD device record. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2021-05-05 + Updated: 2021-05-05 + + Version history: + 1.0.0 - (2021-05-05) Function created + #> + param( + [parameter(Mandatory = $true, HelpMessage = "Specify the Device ID of the Azure AD device record.")] + [ValidateNotNullOrEmpty()] + [string]$DeviceId + ) + Process { + # Retrieve all Windows Autopilot device identities + $ResourceURI = "devices?`$filter=deviceId eq '$($DeviceId)'" + $GraphResponse = (Invoke-MSGraphOperation -Get -APIVersion "v1.0" -Resource $ResourceURI).value + + # Handle return response + return $GraphResponse + } + } + + # Get access token + $AccessToken = Get-AccessToken -TenantID $TenantID + + # Construct array list for all Autopilot device identities with broken associations + $AutopilotDeviceList = New-Object -TypeName "System.Collections.ArrayList" + + # Gather Autopilot device details + Write-Verbose -Message "Attempting to retrieve all Autopilot device identities, this could take some time" + $AutopilotDevices = Get-AutopilotDevice + + # Measure detected Autopilot identities count + $AutopilotIdentitiesCount = ($AutopilotDevices | Measure-Object).Count + + if ($AutopilotDevices -ne $null) { + Write-Verbose -Message "Detected count of Autopilot identities: $($AutopilotIdentitiesCount)" + + # Construct and start a timer for output + $Timer = [System.Diagnostics.Stopwatch]::StartNew() + $AutopilotIdentitiesCurrentCount = 0 + $SecondsCount = 0 + + # Process each Autopilot device identity + foreach ($AutopilotDevice in $AutopilotDevices) { + # Increase current progress count + $AutopilotIdentitiesCurrentCount++ + + # Handle output count for progress visibility + if ([math]::Round($Timer.Elapsed.TotalSeconds) -gt ($SecondsCount + 30)) { + # Increase minutes count for next output frequence + $SecondsCount = [math]::Round($Timer.Elapsed.TotalSeconds) + + # Write output every 30 seconds + Write-Verbose -Message "Elapsed time: $($Timer.Elapsed.Hours) hour $($Timer.Elapsed.Minutes) min $($Timer.Elapsed.Seconds) seconds" + Write-Verbose -Message "Progress count: $($AutopilotIdentitiesCurrentCount) / $($AutopilotIdentitiesCount)" + Write-Verbose -Message "Detected devices: $($AutopilotDeviceList.Count)" + } + + # Handle access token refresh if required + $AccessTokenRenew = Test-AccessToken + if ($AccessTokenRenew -eq $false) { + $AccessToken = Get-AccessToken -TenantID $TenantID -Refresh + } + + # Get Azure AD device record for associated device based on what's set for the Autopilot identity + $AzureADDevice = Get-AzureADDeviceRecord -DeviceId $AutopilotDevice.azureAdDeviceId + if ($AzureADDevice -eq $null) { + # Construct custom object for output + $PSObject = [PSCustomObject]@{ + Id = $AutopilotDevice.id + SerialNumber = $AutopilotDevice.serialNumber + Model = $AutopilotDevice.model + Manufacturer = $AutopilotDevice.manufacturer + } + $AutopilotDeviceList.Add($PSObject) | Out-Null + } + } + + # Handle output at script completion + Write-Verbose -Message "Successfully detected a total of '$($AutopilotDeviceList.Count)' Autopilot identities with a broken Azure AD device association" + Write-Output -InputObject $AutopilotDeviceList + } + else { + Write-Warning -Message "Could not detect any Autopilot device identities" + } +} \ No newline at end of file diff --git a/Autopilot/Upload-WindowsAutopilotDeviceInfo.ps1 b/Autopilot/Upload-WindowsAutopilotDeviceInfo.ps1 new file mode 100644 index 0000000..f79cec5 --- /dev/null +++ b/Autopilot/Upload-WindowsAutopilotDeviceInfo.ps1 @@ -0,0 +1,220 @@ +<#PSScriptInfo +.VERSION 1.1.2 +.GUID 8d3532b3-ff9f-4031-b06f-25fcab76c626 +.AUTHOR NickolajA +.DESCRIPTION Gather device hash from local machine and automatically upload it to Autopilot +.COMPANYNAME SCConfigMgr +.COPYRIGHT +.TAGS Autopilot Windows Intune +.LICENSEURI +.PROJECTURI https://github.com/SCConfigMgr/Intune/blob/master/Autopilot/Upload-WindowsAutopilotDeviceInfo.ps1 +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES +#> +#Requires -Module AzureAD +#Requires -Module PSIntuneAuth +<# +.SYNOPSIS + Gather device hash from local machine and automatically upload it to Autopilot. + +.DESCRIPTION + This script automatically gathers the device hash, serial number, manufacturer and model and uploads that data into Autopilot. + Authentication is required within this script and required permissions for creating Autopilot device identities are needed. + +.PARAMETER TenantName + Specify the tenant name, e.g. tenantname.onmicrosoft.com. + +.PARAMETER ApplicationID + Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration (d1ddf0e4-d672-4dae-b554-9d5bdfd93547). + +.PARAMETER GroupTag + Specify the group tag to easier differentiate Autopilot devices, e.g. 'ABCSales'. + +.PARAMETER UserPrincipalName + Specify the primary user principal name, e.g. 'firstname.lastname@domain.com'. + +.EXAMPLE + # Gather device hash from local computer and upload to Autopilot using Intune Graph API's: + .\Upload-WindowsAutopilotDeviceInfo.ps1 -TenantName "tenant.onmicrosoft.com" + + # Gather device hash from local computer and upload to Autopilot using Intune Graph API's with a given group tag as 'AADUserDriven': + .\Upload-WindowsAutopilotDeviceInfo.ps1 -TenantName "tenant.onmicrosoft.com" -GroupTag "AADUserDriven" + + # Gather device hash from local computer and upload to Autopilot using Intune Graph API's with a given group tag as 'AADUserDriven' and 'somone@domain.com' as the assigned user: + .\Upload-WindowsAutopilotDeviceInfo.ps1 -TenantName "tenant.onmicrosoft.com" -GroupTag "AADUserDriven" -UserPrincipalName "someone@domain.com" + +.NOTES + FileName: Upload-WindowsAutopilotDeviceInfo.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2019-03-21 + Updated: 2021-03-24 + + Version history: + 1.0.0 - (2019-03-21) Script created. + 1.1.0 - (2019-10-29) Added support for specifying the primary user assigned to the uploaded Autopilot device as well as renaming the OrderIdentifier parameter to GroupTag. Thanks to @Stgrdk for his contributions. Switched from Get-CimSession to Get-WmiObject to get device details from WMI. + 1.1.1 - (2021-03-24) Script now uses the groupTag property instead of the depcreated OrderIdentifier property. Also removed the code section that attempted to perform an Autopilot sync operation + 1.1.2 - (2021-03-24) Corrected a spelling mistake of 'GroupTag' to 'groupTag' +#> +[CmdletBinding(SupportsShouldProcess=$true)] +param( + [parameter(Mandatory=$true, HelpMessage="Specify the tenant name, e.g. tenantname.onmicrosoft.com.")] + [ValidateNotNullOrEmpty()] + [string]$TenantName, + + [parameter(Mandatory=$false, HelpMessage="Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration (d1ddf0e4-d672-4dae-b554-9d5bdfd93547).")] + [ValidateNotNullOrEmpty()] + [string]$ApplicationID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547", + + [parameter(Mandatory=$false, HelpMessage="Specify the group tag to easier differentiate Autopilot devices, e.g. 'ABCSales'.")] + [ValidateNotNullOrEmpty()] + [string]$GroupTag, + + [parameter(Mandatory=$false, HelpMessage="Specify the primary user principal name, e.g. 'firstname.lastname@domain.com'.")] + [ValidateNotNullOrEmpty()] + [string]$UserPrincipalName +) +Begin { + # Determine if the AzureAD module needs to be installed + try { + Write-Verbose -Message "Attempting to locate AzureAD module" + $AzureADModule = Get-InstalledModule -Name AzureAD -ErrorAction Stop -Verbose:$false + if ($AzureADModule -ne $null) { + Write-Verbose -Message "AzureAD module detected, checking for latest version" + $LatestModuleVersion = (Find-Module -Name AzureAD -ErrorAction Stop -Verbose:$false).Version + if ($LatestModuleVersion -gt $AzureADModule.Version) { + Write-Verbose -Message "Latest version of AzureAD module is not installed, attempting to install: $($LatestModuleVersion.ToString())" + $UpdateModuleInvocation = Update-Module -Name AzureAD -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + } + } + catch [System.Exception] { + Write-Warning -Message "Unable to detect AzureAD module, attempting to install from PSGallery" + try { + # Install NuGet package provider + $PackageProvider = Install-PackageProvider -Name NuGet -Force -Verbose:$false + + # Install PSIntuneAuth module + Install-Module -Name AzureAD -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + Write-Verbose -Message "Successfully installed AzureAD" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to install AzureAD module. Error message: $($_.Exception.Message)" ; break + } + } + + # Determine if the PSIntuneAuth module needs to be installed + try { + Write-Verbose -Message "Attempting to locate PSIntuneAuth module" + $PSIntuneAuthModule = Get-InstalledModule -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false + if ($PSIntuneAuthModule -ne $null) { + Write-Verbose -Message "Authentication module detected, checking for latest version" + $LatestModuleVersion = (Find-Module -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false).Version + if ($LatestModuleVersion -gt $PSIntuneAuthModule.Version) { + Write-Verbose -Message "Latest version of PSIntuneAuth module is not installed, attempting to install: $($LatestModuleVersion.ToString())" + $UpdateModuleInvocation = Update-Module -Name PSIntuneAuth -Scope CurrentUser -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + } + } + catch [System.Exception] { + Write-Warning -Message "Unable to detect PSIntuneAuth module, attempting to install from PSGallery" + try { + # Install NuGet package provider + $PackageProvider = Install-PackageProvider -Name NuGet -Force -Verbose:$false + + # Install PSIntuneAuth module + Install-Module -Name PSIntuneAuth -Scope AllUsers -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + Write-Verbose -Message "Successfully installed PSIntuneAuth" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to install PSIntuneAuth module. Error message: $($_.Exception.Message)" ; break + } + } + + # Check if token has expired and if, request a new + Write-Verbose -Message "Checking for existing authentication token" + if ($Global:AuthToken -ne $null) { + $UTCDateTime = (Get-Date).ToUniversalTime() + $TokenExpireMins = ($Global:AuthToken.ExpiresOn.datetime - $UTCDateTime).Minutes + Write-Verbose -Message "Current authentication token expires in (minutes): $($TokenExpireMins)" + if ($TokenExpireMins -le 0) { + Write-Verbose -Message "Existing token found but has expired, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID + } + else { + Write-Verbose -Message "Existing authentication token has not expired, will not request a new token" + } + } + else { + Write-Verbose -Message "Authentication token does not exist, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID + } +} +Process { + # Functions + function Get-ErrorResponseBody { + param( + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [System.Exception]$Exception + ) + + # Read the error stream + $ErrorResponseStream = $Exception.Response.GetResponseStream() + $StreamReader = New-Object System.IO.StreamReader($ErrorResponseStream) + $StreamReader.BaseStream.Position = 0 + $StreamReader.DiscardBufferedData() + $ResponseBody = $StreamReader.ReadToEnd(); + + # Handle return object + return $ResponseBody + } + + # Gather device hash data + Write-Verbose -Message "Gather device hash data from local machine" + $DeviceHashData = (Get-WmiObject -Namespace "root/cimv2/mdm/dmmap" -Class "MDM_DevDetail_Ext01" -Filter "InstanceID='Ext' AND ParentID='./DevDetail'" -Verbose:$false).DeviceHardwareData + $SerialNumber = (Get-WmiObject -Class "Win32_BIOS" -Verbose:$false).SerialNumber + $ProductKey = (Get-WmiObject -Class "SoftwareLicensingService" -Verbose:$false).OA3xOriginalProductKey + + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/importedWindowsAutopilotDeviceIdentities" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Construct hash table for new Autopilot device identity and convert to JSON + Write-Verbose -Message "Constructing required JSON body based upon parameter input data for device hash upload" + $AutopilotDeviceIdentity = [ordered]@{ + '@odata.type' = '#microsoft.graph.importedWindowsAutopilotDeviceIdentity' + 'groupTag' = if ($GroupTag) { "$($GroupTag)" } else { "" } + 'serialNumber' = "$($SerialNumber)" + 'productKey' = if ($ProductKey) { "$($ProductKey)" } else { "" } + 'hardwareIdentifier' = "$($DeviceHashData)" + 'assignedUserPrincipalName' = if ($UserPrincipalName) { "$($UserPrincipalName)" } else { "" } + 'state' = @{ + '@odata.type' = 'microsoft.graph.importedWindowsAutopilotDeviceIdentityState' + 'deviceImportStatus' = 'pending' + 'deviceRegistrationId' = '' + 'deviceErrorCode' = 0 + 'deviceErrorName' = '' + } + } + $AutopilotDeviceIdentityJSON = $AutopilotDeviceIdentity | ConvertTo-Json + + try { + # Call Graph API and post JSON data for new Autopilot device identity + Write-Verbose -Message "Attempting to post data for hardware hash upload" + $AutopilotDeviceIdentityResponse = Invoke-RestMethod -Uri $GraphURI -Headers $AuthToken -Method Post -Body $AutopilotDeviceIdentityJSON -ContentType "application/json" -ErrorAction Stop -Verbose:$false + $AutopilotDeviceIdentityResponse + } + catch [System.Exception] { + # Construct stream reader for reading the response body from API call + $ResponseBody = Get-ErrorResponseBody -Exception $_.Exception + + # Handle response output and error message + Write-Output -InputObject "Response content:`n$ResponseBody" + Write-Warning -Message "Failed to upload hardware hash. Request to $($GraphURI) failed with HTTP Status $($_.Exception.Response.StatusCode) and description: $($_.Exception.Response.StatusDescription)" + } +} \ No newline at end of file diff --git a/Certificates/Get-SCEPCertificateDetection.ps1 b/Certificates/Get-SCEPCertificateDetection.ps1 new file mode 100644 index 0000000..cee0c57 --- /dev/null +++ b/Certificates/Get-SCEPCertificateDetection.ps1 @@ -0,0 +1,14 @@ +$TemplateName = "NDES Intune" +$SubjectNames = @("CN=CL", "CN=CORP") +$Certificates = Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object { $_.Subject -match ($SubjectNames -join "|") } +foreach ($Certificate in $Certificates) { + $CertificateTemplateInformation = $Certificate.Extensions | Where-Object { $_.Oid.FriendlyName -match "Certificate Template Information"} + if ($CertificateTemplateInformation -ne $null) { + $CertificateTemplateName = ($CertificateTemplateInformation).Format(0) -replace "(.+)?=(.+)\((.+)?", '$2' + if ($CertificateTemplateName -ne $null) { + if ($CertificateTemplateName -like $TemplateName) { + return 0 + } + } + } +} \ No newline at end of file diff --git a/Certificates/Install-MSIntuneNDESServer.ps1 b/Certificates/Install-MSIntuneNDESServer.ps1 new file mode 100644 index 0000000..e1b27e6 --- /dev/null +++ b/Certificates/Install-MSIntuneNDESServer.ps1 @@ -0,0 +1,439 @@ +<# +.SYNOPSIS + Prepare a Windows server for SCEP certificate distribution using NDES for Microsoft Intune. + +.DESCRIPTION + This script will prepare and configure a Windows server for SCEP certificate distribution using NDES for Microsoft Intune. + For running this script, permissions to set service principal names are required including local administrator privileges on the server where the script is executed on. + +.PARAMETER CertificateAuthorityConfig + Define the Certificate Authority configuration using the following format: \. + +.PARAMETER NDESTemplateName + Define the name of the certificate template that will be used by NDES to issue certificates to mobile devices. Don't specify the display name. + +.PARAMETER NDESExternalFQDN + Define the external FQDN of the NDES service published through an application proxy, e.g. ndes-tenantname.msappproxy.net. + +.PARAMETER RegistrationAuthorityName + Define the Registration Authority name information used by NDES. + +.PARAMETER RegistrationAuthorityCompany + Define the Registration Authority company information used by NDES. + +.PARAMETER RegistrationAuthorityDepartment + Define the Registration Authority department information used by NDES. + +.PARAMETER RegistrationAuthorityCity + Define the Registration Authority city information used by NDES. + +.EXAMPLE + # Install and configure NDES with verbose output: + .\Install-MSIntuneNDESServer.ps1 -CertificateAuthorityConfig "CA01.domain.com\DOMAIN-CA01-CA" -NDESTemplateName "NDESIntune" -NDESExternalFQDN "ndes-tenantname.msappproxy.net" -RegistrationAuthorityName "Name" -RegistrationAuthorityCompany "CompanyName" -RegistrationAuthorityDepartment "Department" -RegistrationAuthorityCity "City" -Verbose + +.NOTES + FileName: Install-MSIntuneNDESServer.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2018-06-17 + Updated: 2018-06-17 + + Version history: + 1.0.0 - (2018-06-17) Script created +#> +[CmdletBinding(SupportsShouldProcess=$true)] +param( + [parameter(Mandatory=$true, HelpMessage="Define the Certificate Authority configuration using the following format: \.")] + [ValidateNotNullOrEmpty()] + [string]$CertificateAuthorityConfig, + + [parameter(Mandatory=$true, HelpMessage="Define the name of the certificate template that will be used by NDES to issue certificates to mobile devices. Don't specify the display name.")] + [ValidateNotNullOrEmpty()] + [string]$NDESTemplateName, + + [parameter(Mandatory=$true, HelpMessage="Define the external FQDN of the NDES service published through an application proxy, e.g. ndes-tenantname.msappproxy.net.")] + [ValidateNotNullOrEmpty()] + [string]$NDESExternalFQDN, + + [parameter(Mandatory=$true, HelpMessage="Define the Registration Authority name information used by NDES.")] + [ValidateNotNullOrEmpty()] + [string]$RegistrationAuthorityName, + + [parameter(Mandatory=$true, HelpMessage="Define the Registration Authority company information used by NDES.")] + [ValidateNotNullOrEmpty()] + [string]$RegistrationAuthorityCompany, + + [parameter(Mandatory=$true, HelpMessage="Define the Registration Authority department information used by NDES.")] + [ValidateNotNullOrEmpty()] + [string]$RegistrationAuthorityDepartment, + + [parameter(Mandatory=$true, HelpMessage="Define the Registration Authority city information used by NDES.")] + [ValidateNotNullOrEmpty()] + [string]$RegistrationAuthorityCity +) +Begin { + # Ensure that running PowerShell version is 5.1 + #Requires -Version 5.1 + + # Init verbose logging for environment gathering process phase + Write-Verbose -Message "Initiating environment gathering process phase" + + # Add additional variables required for installation and configuration + Write-Verbose -Message "- Configuring additional variables required for installation and configuration" + $ServerFQDN = -join($env:COMPUTERNAME, ".", $env:USERDNSDOMAIN.ToLower()) + Write-Verbose -Message "- Variable ServerFQDN has been assigned value: $($ServerFQDN)" + $ServerNTAccountName = -join($env:USERDOMAIN.ToUpper(), "\", $env:COMPUTERNAME, "$") + Write-Verbose -Message "- Variable ServerNTAccountName has been assigned value: $($ServerNTAccountName)" + + # Get Server Authentication certificate for IIS binding + try { + $ServerAuthenticationCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -ErrorAction Stop | Where-Object { ($_.Subject -match $NDESExternalFQDN) -and ($_.Extensions["2.5.29.37"].EnhancedKeyUsages.FriendlyName.Contains("Server Authentication")) } + if ($ServerAuthenticationCertificate -eq $null) { + Write-Warning -Message "Unable to locate required Server Authentication certificate matching external NDES FQDN"; break + } + else { + Write-Verbose -Message "- Successfully located required Server Authentication certificate matching external NDES FQDN" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to locate required Server Authentication certificate matching external NDES FQDN"; break + } + + # Get Client Authentication certifcate for Intune Certificate Connector + try { + $ClientAuthenticationCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" -ErrorAction Stop | Where-Object { ($_.Subject -match $ServerFQDN) -and ($_.Extensions["2.5.29.37"].EnhancedKeyUsages.FriendlyName.Contains("Client Authentication")) } + if ($ClientAuthenticationCertificate -eq $null) { + Write-Warning -Message "Unable to locate required Client Authentication certificate matching internal NDES server FQDN"; break + } + else { + Write-Verbose -Message "- Successfully located required Client Authentication certificate matching internal NDES server FQDN" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to locate required Client Authentication certificate matching internal NDES server FQDN"; break + } + + # Completed verbose logging for environment gathering process phase + Write-Verbose -Message "Completed environment gathering process phase" +} +Process { + # Functions + function Test-PSCredential { + param ( + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential]$Credential + ) + Process { + $ErrorActionPreference = "Stop" + try { + Add-Type -AssemblyName System.DirectoryServices.AccountManagement -ErrorAction Stop + $ContextType = [System.DirectoryServices.AccountManagement.ContextType]::Domain + $PrincipalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $ContextType, $env:USERDNSDOMAIN.ToLower() + $ContextOptions = [System.DirectoryServices.AccountManagement.ContextOptions]::Negotiate + if (-not($PrincipalContext.ValidateCredentials($Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password)) -eq $true) { + return $false + } + else { + return $true + } + } + catch [System.Exception] { + if (-not($PrincipalContext.ValidateCredentials($Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password, $ContextOptions)) -eq $true) { + return $false + } + else { + return $true + } + } + } + } + + # Configure main script error action preference + $ErrorActionPreference = "Stop" + + # Initiate main script function + Write-Verbose -Message "Initiating main script engine to install and configure NDES on server: $($env:COMPUTERNAME)" + + # Init verbose logging for credentials phase + Write-Verbose -Message "Initiating credentials gathering process phase" + + # Get local administrator credential + Write-Verbose -Message "- Prompting for credential input for Enterprise Administrator domain credential" + $AdministratorPSCredential = (Get-Credential -Message "Specify a Enterprise Administrator domain credential with the following formatting 'DOMAIN\useraccount'") + if (-not(Test-PSCredential -Credential $AdministratorPSCredential)) { + Write-Warning -Message "Unable to validate specified Enterprise Administrator domain credentials"; break + } + else { + # Validate local administrator privileges + Write-Verbose -Message "- Given credentials was validated successfully, checking for Enterprise Administrator privileges for current user" + if (-not([Security.Principal.WindowsIdentity]::GetCurrent().Groups | Select-String -Pattern "S-1-5-32-544")) { + Write-Warning -Message "Current user context is not a local administrator on this server"; break + } + } + + # Get service account credential + Write-Verbose -Message "- Prompting for credential input for NDES service account domain credential" + $NDESServiceAccountCredential = (Get-Credential -Message "Specify the NDES service account domain credential with the following formatting 'DOMAIN\useraccount'") + if (-not(Test-PSCredential -Credential $NDESServiceAccountCredential)) { + Write-Warning -Message "Unable to validate specified NDES service account domain credentials"; break + } + $NDESServiceAccountName = -join($NDESServiceAccountCredential.GetNetworkCredential().Domain, "\" ,$NDESServiceAccountCredential.GetNetworkCredential().UserName) + $NDESServiceAccountPassword = $NDESServiceAccountCredential.GetNetworkCredential().SecurePassword + Write-Verbose -Message "- Successfully gathered NDES service account credentials" + + # Completed verbose logging for credentials phase + Write-Verbose -Message "Completed credentials gathering process phase" + + # Init verbose logging for pre-configuration phase + Write-Verbose -Message "Initiating pre-configuration phase" + + # Give computer account read permissions on Client Authentication certificate private key + try { + Write-Verbose -Message "- Attempting to give the NDES server computer account permissions on the Client Authentication certificate private key" + $ClientAuthenticationKeyContainerName = $ClientAuthenticationCertificate.PrivateKey.CspKeyContainerInfo.KeyContainerName + $ClientAuthenticationKeyFilePath = Join-Path -Path $env:ProgramData -ChildPath "Microsoft\Crypto\RSA\MachineKeys\$($ClientAuthenticationKeyContainerName)" + Write-Verbose -Message "- Retrieving existing access rules for private key container" + $ClientAuthenticationACL = Get-Acl -Path $ClientAuthenticationKeyFilePath + + # Check if existing ACL exist matching computer account with read permissions + $ServerAccessRule = $ClientAuthenticationACL.Access | Where-Object { ($_.IdentityReference -like $ServerNTAccountName) -and ($_.FileSystemRights -match "Read") } + if ($ServerAccessRule -eq $null) { + Write-Verbose -Message "- Could not find existing access rule for computer account with read permission on private key, attempting to delegate permissions" + $NTAccountUser = New-Object -TypeName System.Security.Principal.NTAccount($ServerNTAccountName) -ErrorAction Stop + $FileSystemAccessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule($NTAccountUser, "Read", "None", "None", "Allow") -ErrorAction Stop + $ClientAuthenticationACL.AddAccessRule($FileSystemAccessRule) + Set-Acl -Path $ClientAuthenticationKeyFilePath -AclObject $ClientAuthenticationACL -ErrorAction Stop + Write-Verbose -Message "- Successfully delegated the NDES server computer account permissions on the Client Authentication certificate private key" + } + else { + Write-Verbose -Message "- Found an existing access rule for computer account with read permission on private key, will skip configuration" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to give the NDES server computer account permissions on the Client Authentication certificate private key"; break + } + + try { + # Configure service account SPN for local server + Write-Verbose -Message "- Attempting to configure service princal names for NDES service account: $($NDESServiceAccountName)" + Write-Verbose -Message "- Configuring service principal name HTTP/$($ServerFQDN) on $($NDESServiceAccountName)" + $ServerFQDNInvocation = Invoke-Expression -Command "cmd.exe /c setspn.exe -s HTTP/$($ServerFQDN) $($NDESServiceAccountName)" -ErrorAction Stop + if ($ServerFQDNInvocation -match "Updated object") { + Write-Verbose -Message "- Successfully configured service principal name for NDES service account" + } + Write-Verbose -Message "- Configuring service principal name HTTP/$($env:COMPUTERNAME) on $($NDESServiceAccountName)" + $ServerInvocation = Invoke-Expression -Command "cmd.exe /c setspn.exe -s HTTP/$($env:COMPUTERNAME) $($NDESServiceAccountName)" -ErrorAction Stop + if ($ServerInvocation -match "Updated object") { + Write-Verbose -Message "- Successfully configured service principal name for NDES service account" + } + Write-Verbose -Message "- Successfully configured service principal names for NDES service account" + } + catch [System.Exception] { + Write-Warning -Message "Failed to configure service princal names for NDES service account"; break + } + + # Completed verbose logging for pre-configuration phase + Write-Verbose -Message "Completed pre-configuration phase" + + # Init verbose logging for Windows feature installation phase + Write-Verbose -Message "Initiating Windows feature installation phase" + + # Install required Windows features for NDES + $NDESWindowsFeatures = @("ADCS-Device-Enrollment", "Web-Filtering", "Web-Asp-Net", "NET-Framework-Core", "NET-HTTP-Activation", "Web-Asp-Net45", "NET-Framework-45-Core", "NET-Framework-45-ASPNET", "NET-WCF-HTTP-Activation45", "Web-Metabase", "Web-WMI", "Web-Mgmt-Console", "NET-Non-HTTP-Activ") + foreach ($WindowsFeature in $NDESWindowsFeatures) { + Write-Verbose -Message "- Checking installation state for feature: $($WindowsFeature)" + if (((Get-WindowsFeature -Name $WindowsFeature -Verbose:$false).InstallState -ne "Installed")) { + Write-Verbose -Message "- Attempting to install Windows feature: $($WindowsFeature)" + Add-WindowsFeature -Name $WindowsFeature -ErrorAction Stop -Verbose:$false | Out-Null + Write-Verbose -Message "- Successfully installed Windows feature: $($WindowsFeature)" + } + else { + Write-Verbose -Message "- Windows feature is already installed: $($WindowsFeature)" + } + } + + # Completed verbose logging for Windows feature installation phase + Write-Verbose -Message "Completed Windows feature installation phase" + + # Init verbose logging for NDES server role installation phase + Write-Verbose -Message "Initiating NDES server role installation phase" + + # Add NDES service account to the IIS_IUSRS group + try { + Write-Verbose -Message "- Checking if NDES service account is a member of the IIS_IUSRS group" + $IISIUSRSMembers = Get-LocalGroupMember -Group "IIS_IUSRS" -Member $NDESServiceAccountName -ErrorAction SilentlyContinue + if ($IISIUSRSMembers -eq $null) { + Write-Verbose -Message "- Attempting to add NDES service account to the IIS_IUSRS group" + Add-LocalGroupMember -Group "IIS_IUSRS" -Member $NDESServiceAccountName -ErrorAction Stop + Write-Verbose -Message "- Successfully added NDES service account to the IIS_IUSRS group" + } + else { + Write-Verbose -Message "- NDES service account is already a member of the IIS_IUSRS group" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred when attempting to add NDES service account to the IIS_IUSRS group"; break + } + + # Set NDES install parameters + $InstallNDESParams = @{ + "Credential" = $AdministratorPSCredential + "CAConfig" = $CertificateAuthorityConfig + "RAName" = $RegistrationAuthorityName + "RACompany" = $RegistrationAuthorityCompany + "RADepartment" = $RegistrationAuthorityDepartment + "RACity" = $RegistrationAuthorityCity + "ServiceAccountName" = $NDESServiceAccountName + "ServiceAccountPassword" = $NDESServiceAccountPassword + } + + # Install and configure NDES server role + try { + Write-Verbose -Message "- Starting NDES server role installation, this could take some time" + Install-AdcsNetworkDeviceEnrollmentService @InstallNDESParams -Force -ErrorAction Stop -Verbose:$false | Out-Null + Write-Verbose -Message "- Successfully installed and configured NDES server role" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred. Error message: $($_.Exception.Message)"; break + } + + # Completed verbose logging for NDES server role installation phase + Write-Verbose -Message "Completed NDES server role installation phase" + + # Init verbose logging for NDES server role post-installation phase + Write-Verbose -Message "Initiating NDES server role post-installation phase" + + # Configure NDES certificate template in registry + try { + Write-Verbose -Message "- Attempting to configure EncryptionTemplate registry name with value: $($NDESTemplateName)" + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Cryptography\MSCEP" -Name "EncryptionTemplate" -Value $NDESTemplateName -ErrorAction Stop + Write-Verbose -Message "- Successfully configured EncryptionTemplate registry name" + Write-Verbose -Message "- Attempting to configure GeneralPurposeTemplate registry name with value: $($NDESTemplateName)" + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Cryptography\MSCEP" -Name "GeneralPurposeTemplate" -Value $NDESTemplateName -ErrorAction Stop + Write-Verbose -Message "- Successfully configured GeneralPurposeTemplate registry name" + Write-Verbose -Message "- Attempting to configure SignatureTemplate registry name with value: $($NDESTemplateName)" + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Cryptography\MSCEP" -Name "SignatureTemplate" -Value $NDESTemplateName -ErrorAction Stop + Write-Verbose -Message "- Successfully configured SignatureTemplate registry name" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while configuring NDES certificate template in registry"; break + } + + # Completed verbose logging for NDES server role installation phase + Write-Verbose -Message "Completed NDES server role post-installation phase" + + # Init verbose logging for IIS configuration phase + Write-Verbose -Message "Initiating IIS configuration phase" + + # Import required IIS module + try { + Write-Verbose -Message "- Import required IIS module" + Import-Module -Name "WebAdministration" -ErrorAction Stop -Verbose:$false + Write-Verbose -Message "- Successfully imported required IIS module" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while importing the required IIS module"; break + } + + # Configure HTTP parameters in registry + try { + Write-Verbose -Message "- Attempting to configure HTTP parameters in registry, setting MaxFieldLength to value: 65534" + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\HTTP\Parameters" -Name "MaxFieldLength" -Value 65534 -ErrorAction Stop + Write-Verbose -Message "- Successfully configured HTTP parameter in registry for MaxFieldLength" + Write-Verbose -Message "- Attempting to configure HTTP parameters in registry, setting MaxRequestBytes to value: 65534" + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\HTTP\Parameters" -Name "MaxRequestBytes" -Value 65534 -ErrorAction Stop + Write-Verbose -Message "- Successfully configured HTTP parameter in registry for MaxRequestBytes" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while configuring HTTP parameters in registry"; break + } + + # Add new HTTPS binding for Default Web Site + try { + Write-Verbose -Message "- Attempting to create new HTTPS binding for Default Web Site" + $HTTPSWebBinding = Get-WebBinding -Name "Default Web Site" -IPAddress "*" -Port 443 -ErrorAction Stop + if ($HTTPSWebBinding -eq $null) { + New-WebBinding -Name "Default Web Site" -IPAddress "*" -Port 443 -Protocol Https -ErrorAction Stop | Out-Null + Write-Verbose -Message "- Successfully creating new HTTPS binding for Default Web Site" + Write-Verbose -Message "- Attempting to set Server Authentication certificate for HTTPS binding" + $ServerAuthenticationCertificate | New-Item -Path "IIS:\SslBindings\*!443" -ErrorAction Stop | Out-Null + Write-Verbose -Message "- Successfully set Server Authentication certificate for HTTPS binding" + } + else { + Write-Verbose -Message "- Existing HTTPS binding found for Default Web Site, attempting to set Server Authentication certificate" + if (-not(Get-Item -Path "IIS:\SslBindings\*!443" -ErrorAction SilentlyContinue)) { + $ServerAuthenticationCertificate | New-Item -Path "IIS:\SslBindings\*!443" -ErrorAction Stop | Out-Null + Write-Verbose -Message "- Successfully set Server Authentication certificate for HTTPS binding" + } + else { + Write-Verbose -Message "- Existing HTTPS binding already has a certificate selected, removing it" + Remove-Item -Path "IIS:\SslBindings\*!443" -Force -ErrorAction Stop | Out-Null + Write-Verbose -Message "- Successfully removed certificate for existing HTTPS binding" + Write-Verbose -Message "- Attempting to set new Server Authentication certificate for HTTPS binding" + $ServerAuthenticationCertificate | New-Item -Path "IIS:\SslBindings\*!443" -ErrorAction Stop | Out-Null + Write-Verbose -Message "- Successfully set Server Authentication certificate for HTTPS binding" + } + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to create new or update existing HTTPS binding and set certificate selection for Default Web Site"; break + } + + # Configure Default Web Site to require SSL + try { + Write-Verbose -Message "- Attempting to set Default Web Site to require SSL" + Set-WebConfigurationProperty -PSPath "IIS:\" -Filter "/system.webServer/security/access" -Name "sslFlags" -Value "Ssl" -Location "Default Web Site" -ErrorAction Stop + Write-Verbose -Message "- Successfully set Default Web Site to require SSL" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to set Default Web Site to require SSL"; break + } + + # Set Default Web Site request limits + try { + Write-Verbose -Message "- Attempting to set Default Web Site request filtering maximum URL length with value: 65534" + Set-WebConfiguration -PSPath "IIS:\Sites\Default Web Site" -Filter "/system.webServer/security/requestFiltering/requestLimits/@maxUrl" -Value 65534 -ErrorAction Stop + Write-Verbose -Message "- Successfully set Default Web Site request filtering maximum URL length" + Write-Verbose -Message "- Attempting to set Default Web Site request filtering maximum query string with value: 65534" + Set-WebConfiguration -PSPath "IIS:\Sites\Default Web Site" -Filter "/system.webServer/security/requestFiltering/requestLimits/@maxQueryString" -Value 65534 -ErrorAction Stop + Write-Verbose -Message "- Successfully set Default Web Site request filtering maximum query string" + Write-Verbose -Message "- Attempting to set Default Web Site request filtering for double escaping with value: False" + Set-WebConfiguration -PSPath "IIS:\Sites\Default Web Site" -Filter "/system.webServer/security/requestFiltering/@allowDoubleEscaping" -Value "False" -ErrorAction Stop + Write-Verbose -Message "- Successfully set Default Web Site request filtering for double escaping" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to set Default Web Site request filtering configuration"; break + } + + # Configure Default Web Site authentication + try { + # Enable anonymous authentication + Write-Verbose -Message "- Attempting to set Default Web Site anonymous authentication to: Enabled" + Set-WebConfiguration -Location "Default Web Site" -Filter "/system.webServer/security/authentication/anonymousAuthentication/@Enabled" -Value "True" -ErrorAction Stop + Write-Verbose -Message "- Successfully set Default Web Site anonymous authentication" + + # Disable windows authentication + Write-Verbose -Message "- Attempting to set Default Web Site Windows authentication to: Disabled" + Set-WebConfiguration -Location "Default Web Site" -Filter "/system.webServer/security/authentication/windowsAuthentication/@Enabled" -Value "False" -ErrorAction Stop + Write-Verbose -Message "- Successfully set Default Web Site Windows authentication" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to set Default Web Site authentication configuration"; break + } + + # Disable IE Enhanced Security Configuration for administrators + try { + Write-Verbose -Message "- Attempting to disable IE Enhanced Security Configuration for administrators" + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}" -Name "IsInstalled" -Value 0 -ErrorAction Stop + Write-Verbose -Message "- Successfully disabled IE Enhanced Security Configuration for administrators" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to disable IE Enhanced Security Configuration for administrators"; break + } + + # Completed verbose logging for IIS configuration phase + Write-Verbose -Message "Completed IIS configuration phase" + Write-Verbose -Message "Successfully installed and configured this server with NDES for Intune Certificate Connector to be installed" + Write-Verbose -Message "IMPORTANT: Restart the server at this point before installing the Intune Certificate Connector" +} \ No newline at end of file diff --git a/Certificates/Update-SCEPCertificate.ps1 b/Certificates/Update-SCEPCertificate.ps1 new file mode 100644 index 0000000..da97476 --- /dev/null +++ b/Certificates/Update-SCEPCertificate.ps1 @@ -0,0 +1,158 @@ +<# +.SYNOPSIS + Remove existing SCEP device certificate and enroll a new until subject name matches desired configuration. + +.DESCRIPTION + Remove existing SCEP device certificate and enroll a new until subject name matches desired configuration. + +.NOTES + FileName: Update-SCEPCertificate.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2019-12-21 + Updated: 2020-04-24 + + Version history: + 1.0.0 - (2019-12-21) Script created + 1.0.1 - (2020-04-24) Added to check for certificate with subject names matching CN=WIN in addition to CN=DESKTOP and CN=LAPTOP +#> +Process { + # Functions + function Write-CMLogEntry { + param ( + [parameter(Mandatory=$true, HelpMessage="Value added to the log file.")] + [ValidateNotNullOrEmpty()] + [string]$Value, + + [parameter(Mandatory=$true, HelpMessage="Severity for the log entry. 1 for Informational, 2 for Warning and 3 for Error.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("1", "2", "3")] + [string]$Severity, + + [parameter(Mandatory=$false, HelpMessage="Name of the log file that the entry will written to.")] + [ValidateNotNullOrEmpty()] + [string]$FileName = "SCEPCertificateUpdate.log" + ) + # Determine log file location + $WindowsTempLocation = (Join-Path -Path $env:windir -ChildPath "Temp") + $LogFilePath = Join-Path -Path $WindowsTempLocation -ChildPath $FileName + + # Construct time stamp for log entry + $Time = -join @((Get-Date -Format "HH:mm:ss.fff"), "+", (Get-WmiObject -Class Win32_TimeZone | Select-Object -ExpandProperty Bias)) + + # Construct date for log entry + $Date = (Get-Date -Format "MM-dd-yyyy") + + # Construct context for log entry + $Context = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) + + # Construct final log entry + $LogText = "" + + # Add value to log file and if specified console output + try { + if ($Script:PSBoundParameters["Verbose"]) { + # Write either verbose or warning output to console + switch ($Severity) { + 1 { + Write-Verbose -Message $Value + } + default { + Write-Warning -Message $Value + } + } + + # Write output to log file + Out-File -InputObject $LogText -Append -NoClobber -Encoding Default -FilePath $LogFilePath -ErrorAction Stop + } + else { + # Write output to log file + Out-File -InputObject $LogText -Append -NoClobber -Encoding Default -FilePath $LogFilePath -ErrorAction Stop + } + } + catch [System.Exception] { + Write-Warning -Message "Unable to append log entry to SCEPCertificateUpdate.log file. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" + } + } + + function Get-SCEPCertificate { + do { + $SCEPCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object { ($_.Subject -match "CN=DESKTOP") -or ($_.Subject -match "CN=LAPTOP") -or ($_.Subject -match "CN=WIN") } + if ($SCEPCertificate -eq $null) { + Write-CMLogEntry -Value "Unable to locate SCEP certificate, waiting 10 seconds before checking again" -Severity 2 + Start-Sleep -Seconds 10 + } + else { + Write-CMLogEntry -Value "Successfully located SCEP certificate with subject: $($SCEPCertificate.Subject)" -Severity 1 + return $SCEPCertificate + } + } + until ($SCEPCertificate -ne $null) + } + + function Remove-SCEPCertificate { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Object]$InputObject + ) + # Remove SCEP issued certificate + Write-CMLogEntry -Value "Attempting to remove certificate with subject name: $($InputObject.Subject)" -Severity 1 + Remove-Item -Path $InputObject.PSPath -Force + } + + function Test-SCEPCertificate { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]]$Subject + ) + # Force a manual MDM policy sync + Write-CMLogEntry -Value "Triggering manual MDM policy sync" -Severity 1 + Get-ScheduledTask | Where-Object { $_.TaskName -eq "PushLaunch" } | Start-ScheduledTask + + # Check if new SCEP issued certificate was successfully installed + Write-CMLogEntry -Value "Attempting to check if SCEP certificate was successfully installed after a manual MDM policy sync" -Severity 1 + do { + $SCEPCertificateInstallEvent = Get-WinEvent -LogName "Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics-Provider/Admin" | Where-Object { ($_.Id -like "39") -and ($_.TimeCreated -ge (Get-Date).AddMinutes(-1)) } + } + until ($SCEPCertificateInstallEvent -ne $null) + Write-CMLogEntry -Value "SCEP certificate was successfully installed after a manual MDM policy sync, proceeding to validate it's subject name" -Severity 1 + + # Attempt to locate SCEP issued certificate where the subject name matches either 'DESKTOP', 'LAPTOP' or 'WIN' + $SubjectNames = $Subject -join "|" + $SCEPCertificate = Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object { $_.Subject -match $SubjectNames } + if ($SCEPCertificate -eq $null) { + Write-CMLogEntry -Value "SCEP certificate subject name does not match, returning failure" -Severity 3 + return $false + } + else { + Write-CMLogEntry -Value "SCEP certificate subject name matches desired input, returning success" -Severity 1 + return $true + } + } + + # Define the desired subject name matching patterns for a successful SCEP certificate installation + $SubjectNames = @("CN=CL", "CN=CORP") + + # Attempt to locate and wait for SCEP issued certificate where the subject name matches either 'DESKTOP', 'LAPTOP' or 'WIN' + $SCEPCertificateItem = Get-SCEPCertificate + if ($SCEPCertificateItem -ne $null) { + # Remove existing SCEP issues certificate with subject name matching either 'DESKTOP', 'LAPTOP' or 'WIN' + Remove-SCEPCertificate -InputObject $SCEPCertificateItem + + # Validate that new certificate was installed and it contains the correct subject name + do { + $SCEPResult = Test-SCEPCertificate -Subject $SubjectNames + if ($SCEPResult -eq $false) { + # SCEP certificate installed did not match desired subject named, remove it and attempt to enroll a new + Write-CMLogEntry -Value "Failed to validate SCEP certificate subject name, removing existing SCEP certificate" -Severity 3 + Remove-SCEPCertificate -InputObject (Get-SCEPCertificate) + } + else { + Write-CMLogEntry -Value "Successfully validated desired SCEP certificate was successfully installed" -Severity 1 + } + } + until ($SCEPResult -eq $true) + } +} \ No newline at end of file diff --git a/Customization/Set-WindowsDesktopWallpaper.ps1 b/Customization/Set-WindowsDesktopWallpaper.ps1 new file mode 100644 index 0000000..688b7d1 --- /dev/null +++ b/Customization/Set-WindowsDesktopWallpaper.ps1 @@ -0,0 +1,308 @@ +<# +.SYNOPSIS + Replace the default img0.jpg wallpaper image in Windows 10, by downloading the new wallpaper stored in an Azure Storage blob. + +.DESCRIPTION + Downloads a single or multiple desktop wallpaper files located in an Azure Storage Blog container to a folder named Wallpaper in ProgramData. + +.PARAMETER StorageAccountName + Name of the Azure Storage Account. + +.PARAMETER ContainerName + Name of the Azure Storage Blob container. + +.EXAMPLE + .\Set-WindowsDesktopWallpaper.ps1 + +.NOTES + FileName: Set-DesktopWallpaperContent.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-06-04 + Updated: 2020-11-26 + + Version history: + 1.0.0 - (2020-06-04) Script created + 1.1.0 - (2020-11-26) Added support for 4K wallpapers +#> +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [parameter(Mandatory = $false, HelpMessage = "Name of the Azure Storage Account.")] + [ValidateNotNullOrEmpty()] + [string]$StorageAccountName = "", + + [parameter(Mandatory = $false, HelpMessage = "Name of the Azure Storage Blob container.")] + [ValidateNotNullOrEmpty()] + [string]$ContainerName = "" +) +Begin { + # Install required modules for script execution + $Modules = @("NTFSSecurity", "Az.Storage", "Az.Resources") + foreach ($Module in $Modules) { + try { + $CurrentModule = Get-InstalledModule -Name $Module -ErrorAction Stop -Verbose:$false + if ($CurrentModule -ne $null) { + $LatestModuleVersion = (Find-Module -Name $Module -ErrorAction Stop -Verbose:$false).Version + if ($LatestModuleVersion -gt $CurrentModule.Version) { + $UpdateModuleInvocation = Update-Module -Name $Module -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + } + } + catch [System.Exception] { + try { + # Install NuGet package provider + $PackageProvider = Install-PackageProvider -Name NuGet -Force -Verbose:$false + + # Install current missing module + Install-Module -Name $Module -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to install $($Module) module. Error message: $($_.Exception.Message)" + } + } + } + + # Determine the localized name of the principals required for the functionality of this script + $LocalAdministratorsPrincipal = "BUILTIN\Administrators" + $LocalUsersPrincipal = "BUILTIN\Users" + $LocalSystemPrincipal = "NT AUTHORITY\SYSTEM" + $TrustedInstallerPrincipal = "NT SERVICE\TrustedInstaller" + $RestrictedApplicationPackagesPrincipal = "ALL RESTRICTED APPLICATION PACKAGES" + $ApplicationPackagesPrincipal = "ALL APPLICATION PACKAGES" + + # Retrieve storage account context + $StorageAccountContext = New-AzStorageContext -StorageAccountName $StorageAccountName -Anonymous -ErrorAction Stop +} +Process { + # Functions + function Write-LogEntry { + param ( + [parameter(Mandatory = $true, HelpMessage = "Value added to the log file.")] + [ValidateNotNullOrEmpty()] + [string]$Value, + + [parameter(Mandatory = $true, HelpMessage = "Severity for the log entry. 1 for Informational, 2 for Warning and 3 for Error.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("1", "2", "3")] + [string]$Severity, + + [parameter(Mandatory = $false, HelpMessage = "Name of the log file that the entry will written to.")] + [ValidateNotNullOrEmpty()] + [string]$FileName = "Set-WindowsDesktopWallpaper.log" + ) + # Determine log file location + $LogFilePath = Join-Path -Path (Join-Path -Path $env:windir -ChildPath "Temp") -ChildPath $FileName + + # Construct time stamp for log entry + $Time = -join @((Get-Date -Format "HH:mm:ss.fff"), "+", (Get-WmiObject -Class Win32_TimeZone | Select-Object -ExpandProperty Bias)) + + # Construct date for log entry + $Date = (Get-Date -Format "MM-dd-yyyy") + + # Construct context for log entry + $Context = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) + + # Construct final log entry + $LogText = "" + + # Add value to log file + try { + Out-File -InputObject $LogText -Append -NoClobber -Encoding Default -FilePath $LogFilePath -ErrorAction Stop + } + catch [System.Exception] { + Write-Warning -Message "Unable to append log entry to Set-WindowsDesktopWallpaper.log file. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" + } + } + + function Get-AzureBlobContent { + param( + [parameter(Mandatory = $true, HelpMessage = "Name of the Azure Storage Account.")] + [ValidateNotNullOrEmpty()] + [string]$StorageAccountName, + + [parameter(Mandatory = $true, HelpMessage = "Name of the Azure Storage Blob container.")] + [ValidateNotNullOrEmpty()] + [string]$ContainerName + ) + try { + # Construct array list for return value containing file names + $BlobList = New-Object -TypeName System.Collections.ArrayList + + try { + # Retrieve content from storage account blob + $StorageBlobContents = Get-AzStorageBlob -Container $ContainerName -Context $StorageAccountContext -ErrorAction Stop + if ($StorageBlobContents -ne $null) { + foreach ($StorageBlobContent in $StorageBlobContents) { + Write-LogEntry -Value "Adding content file from Azure Storage Blob to return list: $($StorageBlobContent.Name)" -Severity 1 + $BlobList.Add($StorageBlobContent) | Out-Null + } + } + + # Handle return value + return $BlobList + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to retrieve storage account blob contents. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to retrieve storage account context. Error message: $($_.Exception.Message)" -Severity 3 + } + } + + function Invoke-WallpaperFileDownload { + param( + [parameter(Mandatory = $true, HelpMessage = "Name of the image file in the Azure Storage blob.")] + [ValidateNotNullOrEmpty()] + [string]$FileName, + + [parameter(Mandatory = $true, HelpMessage = "Download destination directory for the image file.")] + [ValidateNotNullOrEmpty()] + [string]$Destination + ) + try { + # Download default wallpaper content file from storage account + Write-LogEntry -Value "Downloading content file from Azure Storage Blob: $($FileName)" -Severity 1 + $StorageBlobContent = Get-AzStorageBlobContent -Container $ContainerName -Blob $FileName -Context $StorageAccountContext -Destination $Destination -Force -ErrorAction Stop + + try { + # Grant non-inherited permissions for wallpaper item + $WallpaperImageFilePath = Join-Path -Path $Destination -ChildPath $FileName + Write-LogEntry -Value "Granting '$($LocalSystemPrincipal)' Read and Execute on: $($WallpaperImageFilePath)" -Severity 1 + Add-NTFSAccess -Path $WallpaperImageFilePath -Account $LocalSystemPrincipal -AccessRights "ReadAndExecute" -ErrorAction Stop + Write-LogEntry -Value "Granting '$($LocalAdministratorsPrincipal)' Read and Execute on: $($WallpaperImageFilePath)" -Severity 1 + Add-NTFSAccess -Path $WallpaperImageFilePath -Account $LocalAdministratorsPrincipal -AccessRights "ReadAndExecute" -ErrorAction Stop + Write-LogEntry -Value "Granting '$($LocalUsersPrincipal)' Read and Execute on: $($WallpaperImageFilePath)" -Severity 1 + Add-NTFSAccess -Path $WallpaperImageFilePath -Account $LocalUsersPrincipal -AccessRights "ReadAndExecute" -ErrorAction Stop + Write-LogEntry -Value "Granting '$($ApplicationPackagesPrincipal)' Read and Execute on: $($WallpaperImageFilePath)" -Severity 1 + Add-NTFSAccess -Path $WallpaperImageFilePath -Account $ApplicationPackagesPrincipal -AccessRights "ReadAndExecute" -ErrorAction Stop + Write-LogEntry -Value "Granting '$($RestrictedApplicationPackagesPrincipal)' Read and Execute on: $($WallpaperImageFilePath)" -Severity 1 + Add-NTFSAccess -Path $WallpaperImageFilePath -Account $RestrictedApplicationPackagesPrincipal -AccessRights "ReadAndExecute" -ErrorAction Stop + Write-LogEntry -Value "Granting '$($TrustedInstallerPrincipal)' Full Control on: $($WallpaperImageFilePath)" -Severity 1 + Add-NTFSAccess -Path $WallpaperImageFilePath -Account $TrustedInstallerPrincipal -AccessRights "FullControl" -ErrorAction Stop + Write-LogEntry -Value "Disabling inheritance on: $($WallpaperImageFilePath)" -Severity 1 + Disable-NTFSAccessInheritance -Path $WallpaperImageFilePath -RemoveInheritedAccessRules -ErrorAction Stop + + try { + # Set owner to trusted installer for new wallpaper file + Write-LogEntry -Value "Setting ownership for '$($TrustedInstallerPrincipal)' on wallpaper image file: $($WallpaperImageFilePath)" -Severity 1 + Set-NTFSOwner -Path $WallpaperImageFilePath -Account $TrustedInstallerPrincipal -ErrorAction Stop + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to set ownership for '$($TrustedInstallerPrincipal)' on wallpaper image file: $($WallpaperImageFilePath). Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to revert permissions for wallpaper image file. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to downloaded wallpaper content from Azure Storage Blob. Error message: $($_.Exception.Message)" -Severity 3 + } + } + + function Remove-WallpaperFile { + param( + [parameter(Mandatory = $true, HelpMessage = "Full path to the image file to be removed.")] + [ValidateNotNullOrEmpty()] + [string]$FilePath + ) + try { + # Take ownership of the wallpaper file + Write-LogEntry -Value "Determining if ownership needs to be changed for file: $($FilePath)" -Severity 1 + $CurrentOwner = Get-Item -Path $FilePath | Get-NTFSOwner + if ($CurrentOwner.Owner -notlike $LocalAdministratorsPrincipal) { + Write-LogEntry -Value "Amending owner as '$($LocalAdministratorsPrincipal)' temporarily for: $($FilePath)" -Severity 1 + Set-NTFSOwner -Path $FilePath -Account $LocalAdministratorsPrincipal -ErrorAction Stop + } + + try { + # Grant local Administrators group and system full control + Write-LogEntry -Value "Granting '$($LocalSystemPrincipal)' Full Control on: $($FilePath)" -Severity 1 + Add-NTFSAccess -Path $FilePath -Account $LocalSystemPrincipal -AccessRights "FullControl" -AccessType "Allow" -ErrorAction Stop + Write-LogEntry -Value "Granting '$($LocalAdministratorsPrincipal)' Full Control on: $($FilePath)" -Severity 1 + Add-NTFSAccess -Path $FilePath -Account $LocalAdministratorsPrincipal -AccessRights "FullControl" -AccessType "Allow" -ErrorAction Stop + + try { + # Remove existing local default wallpaper file + Write-LogEntry -Value "Attempting to remove existing default wallpaper image file: $($FilePath)" -Severity 1 + Remove-Item -Path $FilePath -Force -ErrorAction Stop + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to remove wallpaper image file '$($FilePath)'. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to grant Administrators and local system with full control for wallpaper image file. Error message: $($_.Exception.Message)" -Severity 3 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to take ownership of '$($FilePath)'. Error message: $($_.Exception.Message)" -Severity 3 + } + } + + # Check if desktop wallpaper content exists on the specified storage account + $AzureStorageBlobContent = Get-AzureBlobContent -StorageAccountName $StorageAccountName -ContainerName $ContainerName + if ($AzureStorageBlobContent -ne $null) { + # Replace default wallpaper content locally with item from storage account + $DefaultWallpaperBlobFile = $AzureStorageBlobContent | Where-Object { $PSItem.Name -like "img0.jpg" } + if ($DefaultWallpaperBlobFile -ne $null) { + Write-LogEntry -Value "Detected default wallpaper file 'img0' in container, will replace local wallpaper file" -Severity 1 + + # Remove default wallpaper image file + $DefaultWallpaperImagePath = Join-Path -Path $env:windir -ChildPath "Web\Wallpaper\Windows\img0.jpg" + Remove-WallpaperFile -FilePath $DefaultWallpaperImagePath + + # Download new wallpaper content from storage account + Invoke-WallpaperFileDownload -FileName $DefaultWallpaperBlobFile.Name -Destination (Split-Path -Path $DefaultWallpaperImagePath -Parent) + } + + # Check if additional wallpaper files are present in the Azure Storage blob and replace those in the default location + $WallpaperBlobFiles = $AzureStorageBlobContent | Where-Object { $PSItem.Name -match "^img(\d?[1-9]|[1-9]0).jpg$" } + if ($WallpaperBlobFiles -ne $null) { + Write-LogEntry -Value "Detected theme wallpaper files in container, will replace matching local theme wallpaper files" -Severity 1 + + # Remove all items in '%windir%\Web\Wallpaper\Theme1' (Windows 10) directory and replace with wallpaper content from storage account + $ThemeWallpaperImagePath = Join-Path -Path $env:windir -ChildPath "Web\Wallpaper\Theme1" + $ThemeWallpaperImages = Get-ChildItem -Path $ThemeWallpaperImagePath -Filter "*.jpg" + foreach ($ThemeWallpaperImage in $ThemeWallpaperImages) { + # Remove current theme wallpaper image file + Remove-WallpaperFile -FilePath $ThemeWallpaperImage.FullName + } + + foreach ($WallpaperBlobFile in $WallpaperBlobFiles) { + # Download new wallpaper content from storage account + Invoke-WallpaperFileDownload -FileName $WallpaperBlobFile.Name -Destination $ThemeWallpaperImagePath + } + } + + # Check if 4K wallpaper files are present in the Azure Storage blog and replace those in the default location + $WallpaperBlob4KFiles = $AzureStorageBlobContent | Where-Object { $PSItem.Name -match "^img0_(\d+)x(\d+).*.jpg$" } + if ($WallpaperBlob4KFiles -ne $null) { + Write-LogEntry -Value "Detected 4K wallpaper files in container, will replace matching local wallpaper file" -Severity 1 + + # Define 4K wallpaper path and retrieve all image files + $4KWallpaperImagePath = Join-Path -Path $env:windir -ChildPath "Web\4K\Wallpaper\Windows" + $4KWallpaperImages = Get-ChildItem -Path $4KWallpaperImagePath -Filter "*.jpg" + + foreach ($WallpaperBlob4KFile in $WallpaperBlob4KFiles) { + # Remove current 4K wallpaper image file and replace with image from storage account + if ($WallpaperBlob4KFile.Name -in $4KWallpaperImages.Name) { + Write-LogEntry -Value "Current container item with name '$($WallpaperBlob4KFile.Name)' matches local wallpaper item, starting replacement process" -Severity 1 + + # Get matching local wallpaper image for current container item + $4KWallpaperImage = $4KWallpaperImages | Where-Object { $PSItem.Name -like $WallpaperBlob4KFile.Name } + + # Remove current theme wallpaper image file + Remove-WallpaperFile -FilePath $4KWallpaperImage.FullName + + # Download new wallpaper content from storage account + Invoke-WallpaperFileDownload -FileName $WallpaperBlob4KFile.Name -Destination $4KWallpaperImagePath + } + else { + Write-LogEntry -Value "Downloaded 4K wallpaper with file name '$($WallpaperBlob4KFile.Name)' doesn't match any of the built-in 4K wallpaper image file names, skipping" -Severity 2 + } + } + } + } +} \ No newline at end of file diff --git a/Device Configuration/Export-IntuneDeviceConfigurationProfile.ps1 b/Device Configuration/Export-IntuneDeviceConfigurationProfile.ps1 new file mode 100644 index 0000000..3888cd8 --- /dev/null +++ b/Device Configuration/Export-IntuneDeviceConfigurationProfile.ps1 @@ -0,0 +1,459 @@ +<# +.SYNOPSIS + Export device configuration profiles for Windows, iOS/iPadOS, AndroidEnterprise, macOS platforms in Intune to a local path as JSON files. + +.DESCRIPTION + Export device configuration profiles for Windows, iOS/iPadOS, AndroidEnterprise, macOS platforms in Intune to a local path as JSON files. + +.PARAMETER TenantName + Specify the tenant name, e.g. domain.onmicrosoft.com. + +.PARAMETER Platform + Specify the given platforms that device configuration profiles should be exported for. + +.PARAMETER Path + Specify an existing local path to where the exported Device Configuration JSON files will be stored. + +.PARAMETER SkipPrefix + When specified, the prefix (e.g. COMPANY-) in the following naming convention 'COMPANY-W10-Custom' will be removed. + +.PARAMETER ApplicationID + Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration. + +.PARAMETER PromptBehavior + Set the prompt behavior when acquiring a token. + +.EXAMPLE + # Export all device configuration profiles for all platforms from a tenant named 'domain.onmicrosoft.com' to local path 'C:\Temp\Intune': + .\Export-IntuneDeviceConfigurationProfile.ps1 -TenantName "domain.onmicrosoft.com" -Platform "Windows", "iOS", "AndroidEnterprise", "macOS" -Path C:\Temp\Intune -Verbose + +.NOTES + FileName: Export-IntuneDeviceConfigurationProfile.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2019-10-04 + Updated: 2019-10-04 + + Version history: + 1.0.0 - (2019-10-04) Script created + + Required modules: + AzureAD (Install-Module -Name AzureAD) + PSIntuneAuth (Install-Module -Name PSIntuneAuth) +#> +[CmdletBinding(SupportsShouldProcess=$true)] +param( + [parameter(Mandatory = $true, HelpMessage = "Specify the tenant name, e.g. domain.onmicrosoft.com.")] + [ValidateNotNullOrEmpty()] + [string]$TenantName, + + [parameter(Mandatory = $false, HelpMessage = "Specify the given platforms that device configuration profiles should be exported for.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Windows", "iOS", "AndroidEnterprise", "macOS")] + [string[]]$Platform, + + [parameter(Mandatory = $true, HelpMessage = "Specify an existing local path to where the exported Device Configuration JSON files will be stored.")] + [ValidateNotNullOrEmpty()] + [ValidatePattern("^[A-Za-z]{1}:\\\w+")] + [ValidateScript({ + # Check if path contains any invalid characters + if ((Split-Path -Path $_ -Leaf).IndexOfAny([IO.Path]::GetInvalidFileNameChars()) -ge 0) { + Write-Warning -Message "$(Split-Path -Path $_ -Leaf) contains invalid characters" + } + else { + # Check if the whole path exists + if (Test-Path -Path $_ -PathType Container) { + return $true + } + else { + Write-Warning -Message "Unable to locate part of or the whole specified path, specify a valid path" + } + } + })] + [string]$Path, + + [parameter(Mandatory = $false, HelpMessage = "When specified, the prefix (e.g. COMPANY-) in the following naming convention 'COMPANY-W10-Custom' will be removed.")] + [ValidateNotNullOrEmpty()] + [string]$SkipPrefix, + + [parameter(Mandatory = $false, HelpMessage = "Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration.")] + [ValidateNotNullOrEmpty()] + [string]$ApplicationID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547", + + [parameter(Mandatory=$false, HelpMessage="Set the prompt behavior when acquiring a token.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Auto", "Always", "Never", "RefreshSession")] + [string]$PromptBehavior = "Auto" +) +Begin { + # Determine if the PSIntuneAuth module needs to be installed + try { + Write-Verbose -Message "Attempting to locate PSIntuneAuth module" + $PSIntuneAuthModule = Get-InstalledModule -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false + if ($PSIntuneAuthModule -ne $null) { + Write-Verbose -Message "Authentication module detected, checking for latest version" + $LatestModuleVersion = (Find-Module -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false).Version + if ($LatestModuleVersion -gt $PSIntuneAuthModule.Version) { + Write-Verbose -Message "Latest version of PSIntuneAuth module is not installed, attempting to install: $($LatestModuleVersion.ToString())" + $UpdateModuleInvocation = Update-Module -Name PSIntuneAuth -Scope CurrentUser -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + } + } + catch [System.Exception] { + Write-Warning -Message "Unable to detect PSIntuneAuth module, attempting to install from PSGallery" + try { + # Install NuGet package provider + $PackageProvider = Install-PackageProvider -Name NuGet -Force -Verbose:$false + + # Install PSIntuneAuth module + Install-Module -Name PSIntuneAuth -Scope AllUsers -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + Write-Verbose -Message "Successfully installed PSIntuneAuth" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to install PSIntuneAuth module. Error message: $($_.Exception.Message)" ; break + } + } + + # Check if token has expired and if, request a new + Write-Verbose -Message "Checking for existing authentication token" + if ($Global:AuthToken -ne $null) { + $UTCDateTime = (Get-Date).ToUniversalTime() + $TokenExpireMins = ($Global:AuthToken.ExpiresOn.datetime - $UTCDateTime).Minutes + Write-Verbose -Message "Current authentication token expires in (minutes): $($TokenExpireMins)" + if ($TokenExpireMins -le 0) { + Write-Verbose -Message "Existing token found but has expired, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } + else { + if ($PromptBehavior -like "Always") { + Write-Verbose -Message "Existing authentication token has not expired but prompt behavior was set to always ask for authentication, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } + else { + Write-Verbose -Message "Existing authentication token has not expired, will not request a new token" + } + } + } + else { + Write-Verbose -Message "Authentication token does not exist, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } +} +Process { + # Functions + function Get-ErrorResponseBody { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Exception]$Exception + ) + + # Read the error stream + $ErrorResponseStream = $Exception.Response.GetResponseStream() + $StreamReader = New-Object System.IO.StreamReader($ErrorResponseStream) + $StreamReader.BaseStream.Position = 0 + $StreamReader.DiscardBufferedData() + $ResponseBody = $StreamReader.ReadToEnd() + + # Handle return object + return $ResponseBody + } + + function Invoke-IntuneGraphRequest { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$URI + ) + try { + # Construct array list for return values + $ResponseList = New-Object -TypeName System.Collections.ArrayList + + # Call Graph API and get JSON response + $GraphResponse = Invoke-RestMethod -Uri $URI -Headers $AuthToken -Method Get -ErrorAction Stop -Verbose:$false + if ($GraphResponse -ne $null) { + if ($GraphResponse.value -ne $null) { + foreach ($Response in $GraphResponse.value) { + $ResponseList.Add($Response) | Out-Null + } + } + else { + $ResponseList.Add($GraphResponse) | Out-Null + } + } + + # Handle return objects from response + return $ResponseList + } + catch [System.Exception] { + # Construct stream reader for reading the response body from API call + $ResponseBody = Get-ErrorResponseBody -Exception $_.Exception + + # Handle response output and error message + Write-Output -InputObject "Response content:`n$ResponseBody" + Write-Warning -Message "Request to $($URI) failed with HTTP Status $($_.Exception.Response.StatusCode) and description: $($_.Exception.Response.StatusDescription)" + } + } + + function Export-JSON { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Object]$InputObject, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Path, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [ValidateSet("General", "AdministrativeTemplate")] + [string]$Type + ) + try { + # Handle removal of prefix from display name + if ($Type -like "General") { + if ($Script:PSBoundParameters["SkipPrefix"]) { + $InputObject.displayName = $InputObject.displayName.Replace($SkipPrefix, "") + $Name = $Name.Replace($SkipPrefix, "") + } + } + + # Convert input data to JSON and remove unwanted properties + $JSONData = ($InputObject | Select-Object -Property * -ExcludeProperty id, createdDateTime, lastModifiedDateTime, version, supportsScopeTags | ConvertTo-Json -Depth 10).Replace("\u0027","'") + + # Construct file name + $FilePath = Join-Path -Path $Path -ChildPath (-join($Name, ".json")) + + # Output to file + Write-Verbose -Message "Exporting device configuration profile with name: $($Name)" + $JSONData | Set-Content -Path $FilePath -Encoding "Ascii" -Force -ErrorAction Stop + } + catch [System.Exception] { + Write-Warning -Message "Failed to export JSON input data to path '$($FilePath)'. Error message: $($_.Exception.Message)" + } + } + + function Get-IntuneDeviceConfigurationProfile { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Platform + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/deviceConfigurations" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + $ResponseList = New-Object -TypeName System.Collections.ArrayList + $GraphResponse = Invoke-IntuneGraphRequest -URI $GraphURI + + if ($GraphResponse -ne $null) { + foreach ($ResponseItem in $GraphResponse) { + switch -Regex ($ResponseItem.'@odata.type') { + "microsoft.graph.androidDeviceOwner" { + $PlatformType = "AndroidEnterprise" + } + "microsoft.graph.androidWorkProfile" { + $PlatformType = "AndroidEnterprise" + } + "microsoft.graph.windows" { + $PlatformType = "Windows" + } + "microsoft.graph.ios" { + $PlatformType = "iOS" + } + } + + if ($PlatformItem -like $PlatformType) { + $ResponseList.Add($ResponseItem) | Out-Null + } + } + } + + # Handle return objects from response + return $ResponseList + } + + function Get-IntuneAdministrativeTemplateProfiles { + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/groupPolicyConfigurations" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + $ResponseList = New-Object -TypeName System.Collections.ArrayList + $GraphResponse = Invoke-IntuneGraphRequest -URI $GraphURI + + foreach ($ResponseItem in $GraphResponse) { + $ResponseList.Add($ResponseItem) | Out-Null + } + + # Handle return objects from response + return $ResponseList + } + + function Get-IntuneAdministrativeTemplateDefinitionValues { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AdministrativeTemplateId + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/groupPolicyConfigurations/$($AdministrativeTemplateId)/definitionValues" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + Invoke-IntuneGraphRequest -URI $GraphURI + } + + function Get-IntuneAdministrativeTemplateDefinitionValuesPresentationValues { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AdministrativeTemplateId, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$DefinitionValueID + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/groupPolicyConfigurations/$($AdministrativeTemplateId)/definitionValues/$($DefinitionValueID)/presentationValues" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + Invoke-IntuneGraphRequest -URI $GraphURI + } + + function Get-IntuneAdministrativeTemplateDefinitionValuesDefinition { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AdministrativeTemplateId, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$DefinitionValueID + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/groupPolicyConfigurations/$($AdministrativeTemplateId)/definitionValues/$($DefinitionValueID)/definition" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + Invoke-IntuneGraphRequest -URI $GraphURI + } + + function Get-IntuneAdministrativeTemplateDefinitionsPresentations { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AdministrativeTemplateId, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$DefinitionValueID + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/groupPolicyConfigurations/$($AdministrativeTemplateId)/definitionValues/$($DefinitionValueID)/presentationValues?`$expand=presentation" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + (Invoke-IntuneGraphRequest -URI $GraphURI).presentation + } + + # Process export operation based on specified platforms + foreach ($PlatformItem in $Platform) { + Write-Verbose -Message "Currently processing device configuration profiles for platform: $($PlatformItem)" + + # Retrieve all device configuration profiles for current platform + $DeviceConfigurationProfiles = Get-IntuneDeviceConfigurationProfile -Platform $PlatformItem + + if (($DeviceConfigurationProfiles | Measure-Object).Count -ge 1) { + foreach ($DeviceConfigurationProfile in $DeviceConfigurationProfiles) { + $DeviceConfigurationProfileName = $DeviceConfigurationProfile.displayName + Export-JSON -InputObject $DeviceConfigurationProfile -Path $Path -Name $DeviceConfigurationProfileName -Type "General" + } + } + else { + Write-Warning -Message "Empty query result for device configuration profiles for platform: $($PlatformItem)" + } + + # Retrieve all device configuration administrative templates for current platform + if ($PlatformItem -like "Windows") { + $AdministrativeTemplateProfiles = Get-IntuneAdministrativeTemplateProfiles + if (($AdministrativeTemplateProfiles | Measure-Object).Count -ge 1) { + foreach ($AdministrativeTemplateProfile in $AdministrativeTemplateProfiles) { + Write-Verbose -Message "Exporting administrative template with name: $($AdministrativeTemplateProfile.displayName)" + + # Handle removal of prefix + $AdministrativeTemplateProfileName = $AdministrativeTemplateProfile.displayName + if ($PSBoundParameters["SkipPrefix"]) { + $AdministrativeTemplateProfileName = $AdministrativeTemplateProfile.displayName.Replace($SkipPrefix ,"") + } + + # Define new folder with administrative template profile name to contain any subsequent JSON files + $AdministrativeTemplateProfileFolderPath = Join-Path -Path $Path -ChildPath $AdministrativeTemplateProfileName + if (-not(Test-Path -Path $AdministrativeTemplateProfileFolderPath)) { + New-Item -Path $AdministrativeTemplateProfileFolderPath -ItemType Directory -Force | Out-Null + } + + # Retrieve all definition values for current administrative template and loop through them + $AdministrativeTemplateDefinitionValues = Get-IntuneAdministrativeTemplateDefinitionValues -AdministrativeTemplateId $AdministrativeTemplateProfile.id + foreach ($AdministrativeTemplateDefinitionValue in $AdministrativeTemplateDefinitionValues) { + # Retrieve the defintion of the current definition value + $DefinitionValuesDefinition = Get-IntuneAdministrativeTemplateDefinitionValuesDefinition -AdministrativeTemplateId $AdministrativeTemplateProfile.id -DefinitionValueID $AdministrativeTemplateDefinitionValue.id + $DefinitionValuesDefinitionID = $DefinitionValuesDefinition.id + $DefinitionValuesDefinitionDisplayName = $DefinitionValuesDefinition.displayName + + # Retrieve the presentations of the current definition value + $DefinitionsPresentations = Get-IntuneAdministrativeTemplateDefinitionsPresentations -AdministrativeTemplateId $AdministrativeTemplateProfile.id -DefinitionValueID $AdministrativeTemplateDefinitionValue.id + + # Rertrieve the presentation values of the current definition value + $DefinitionValuesPresentationValues = Get-IntuneAdministrativeTemplateDefinitionValuesPresentationValues -AdministrativeTemplateId $AdministrativeTemplateProfile.id -DefinitionValueID $AdministrativeTemplateDefinitionValue.id + + # Create custom definition object to be exported + $PSObject = New-Object -TypeName PSCustomObject + $PSObject | Add-Member -MemberType "NoteProperty" -Name "definition@odata.bind" -Value "https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('$($DefinitionValuesDefinition.id)')" + $PSObject | Add-Member -MemberType "NoteProperty" -Name "enabled" -Value $($AdministrativeTemplateDefinitionValue.enabled.ToString().ToLower()) + + # Check whether presentation values exist for current definition value + if (($DefinitionValuesPresentationValues.id | Measure-Object).Count -ge 1) { + $i = 0 + $PresentationValues = New-Object -TypeName System.Collections.ArrayList + foreach ($PresentationValue in $DefinitionValuesPresentationValues) { + # Handle multiple items in case of an array + if (($DefinitionsPresentations.id).Count -ge 1) { + $DefinitionsPresentationsID = $DefinitionsPresentations[$i].id + } + else { + $DefinitionsPresentationsID = $DefinitionsPresentations.id + } + + # Construct new presentation value object + $CurrentObject = $PresentationValue | Select-Object -Property * -ExcludeProperty id, createdDateTime, lastModifiedDateTime, version + $CurrentObject | Add-Member -MemberType "NoteProperty" -Name "presentation@odata.bind" -Value "https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('$($DefinitionValuesDefinition.id)')/presentations('$($DefinitionsPresentationsID)')" + $PresentationValues.Add($CurrentObject) | Out-Null + $i++ + } + + # Add all presentation value objects to custom object + $PSObject | Add-Member -MemberType NoteProperty -Name "presentationValues" -Value $PresentationValues + } + + Write-Verbose -Message "Exporting administrative template setting with name: $($DefinitionValuesDefinitionDisplayName)" + Export-JSON -InputObject $PSObject -Path $AdministrativeTemplateProfileFolderPath -Name $DefinitionValuesDefinitionDisplayName -Type "AdministrativeTemplate" + } + } + } + } + } +} \ No newline at end of file diff --git a/Device Configuration/Import-IntuneDeviceConfigurationProfile.ps1 b/Device Configuration/Import-IntuneDeviceConfigurationProfile.ps1 new file mode 100644 index 0000000..39cbbcf --- /dev/null +++ b/Device Configuration/Import-IntuneDeviceConfigurationProfile.ps1 @@ -0,0 +1,443 @@ +<# +.SYNOPSIS + Import device configuration profiles for Windows, iOS/iPadOS, AndroidEnterprise, macOS platforms stored as JSON files into a specific Intune tenant. + +.DESCRIPTION + Import device configuration profiles for Windows, iOS/iPadOS, AndroidEnterprise, macOS platforms stored as JSON files into a specific Intune tenant. + +.PARAMETER TenantName + Specify the tenant name, e.g. domain.onmicrosoft.com. + +.PARAMETER Platform + Specify the given platforms that device configuration profiles should be imported for. + +.PARAMETER Path + Specify an existing local path to where the Device Configuration JSON files are located. + +.PARAMETER Prefix + Specify the prefix that will be added to the device configuration profile name. + +.PARAMETER ApplicationID + Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration. + +.PARAMETER PromptBehavior + Set the prompt behavior when acquiring a token. + +.EXAMPLE + # Import all device configuration profiles for all platforms from 'C:\Temp\Intune' into a tenant named 'domain.onmicrosoft.com': + .\Import-IntuneDeviceConfigurationProfile.ps1 -TenantName "domain.onmicrosoft.com" -Platform "Windows", "iOS", "AndroidEnterprise", "macOS" -Path C:\Temp\Intune -Prefix "CompanyName" -Verbose + +.NOTES + FileName: Import-IntuneDeviceConfigurationProfile.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2019-10-04 + Updated: 2019-10-04 + + Version history: + 1.0.0 - (2019-10-04) Script created + + Required modules: + AzureAD (Install-Module -Name AzureAD) + PSIntuneAuth (Install-Module -Name PSIntuneAuth) +#> +[CmdletBinding(SupportsShouldProcess=$true)] +param( + [parameter(Mandatory = $true, HelpMessage = "Specify the tenant name, e.g. domain.onmicrosoft.com.")] + [ValidateNotNullOrEmpty()] + [string]$TenantName, + + [parameter(Mandatory = $false, HelpMessage = "Specify the given platforms that device configuration profiles should be imported for.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Windows", "iOS", "AndroidEnterprise", "macOS")] + [string[]]$Platform, + + [parameter(Mandatory = $true, HelpMessage = "Specify an existing local path to where the Device Configuration JSON files are located.")] + [ValidateNotNullOrEmpty()] + [ValidatePattern("^[A-Za-z]{1}:\\\w+")] + [ValidateScript({ + # Check if path contains any invalid characters + if ((Split-Path -Path $_ -Leaf).IndexOfAny([IO.Path]::GetInvalidFileNameChars()) -ge 0) { + Write-Warning -Message "$(Split-Path -Path $_ -Leaf) contains invalid characters" + } + else { + # Check if the whole path exists + if (Test-Path -Path $_ -PathType Container) { + return $true + } + else { + Write-Warning -Message "Unable to locate part of or the whole specified path, specify a valid path" + } + } + })] + [string]$Path, + + [parameter(Mandatory = $false, HelpMessage = "Specify the prefix that will be added to the device configuration profile name.")] + [ValidateNotNullOrEmpty()] + [string]$Prefix, + + [parameter(Mandatory = $false, HelpMessage = "Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration.")] + [ValidateNotNullOrEmpty()] + [string]$ApplicationID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547", + + [parameter(Mandatory=$false, HelpMessage="Set the prompt behavior when acquiring a token.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Auto", "Always", "Never", "RefreshSession")] + [string]$PromptBehavior = "Auto" +) +Begin { + # Determine if the PSIntuneAuth module needs to be installed + try { + Write-Verbose -Message "Attempting to locate PSIntuneAuth module" + $PSIntuneAuthModule = Get-InstalledModule -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false + if ($PSIntuneAuthModule -ne $null) { + Write-Verbose -Message "Authentication module detected, checking for latest version" + $LatestModuleVersion = (Find-Module -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false).Version + if ($LatestModuleVersion -gt $PSIntuneAuthModule.Version) { + Write-Verbose -Message "Latest version of PSIntuneAuth module is not installed, attempting to install: $($LatestModuleVersion.ToString())" + $UpdateModuleInvocation = Update-Module -Name PSIntuneAuth -Scope CurrentUser -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + } + } + catch [System.Exception] { + Write-Warning -Message "Unable to detect PSIntuneAuth module, attempting to install from PSGallery" + try { + # Install NuGet package provider + $PackageProvider = Install-PackageProvider -Name NuGet -Force -Verbose:$false + + # Install PSIntuneAuth module + Install-Module -Name PSIntuneAuth -Scope AllUsers -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + Write-Verbose -Message "Successfully installed PSIntuneAuth" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to install PSIntuneAuth module. Error message: $($_.Exception.Message)"; break + } + } + + # Check if token has expired and if, request a new + Write-Verbose -Message "Checking for existing authentication token" + if ($Global:AuthToken -ne $null) { + $UTCDateTime = (Get-Date).ToUniversalTime() + $TokenExpireMins = ($Global:AuthToken.ExpiresOn.datetime - $UTCDateTime).Minutes + Write-Verbose -Message "Current authentication token expires in (minutes): $($TokenExpireMins)" + if ($TokenExpireMins -le 0) { + Write-Verbose -Message "Existing token found but has expired, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } + else { + if ($PromptBehavior -like "Always") { + Write-Verbose -Message "Existing authentication token has not expired but prompt behavior was set to always ask for authentication, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } + else { + Write-Verbose -Message "Existing authentication token has not expired, will not request a new token" + } + } + } + else { + Write-Verbose -Message "Authentication token does not exist, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } + + # Validate that given path contains JSON files + try { + $JSONFiles = Get-ChildItem -Path $Path -Filter *.json -ErrorAction Stop + if ($JSONFiles -eq $null) { + $SkipDeviceConfigurationProfiles = $true + Write-Warning -Message "Specified path doesn't contain any .json files, skipping device configuration profile import actions" + } + else { + $SkipDeviceConfigurationProfiles = $false + Write-Verbose -Message "Specified path contains .json files for device configuration profiles, will include those for import" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to validate .json files existence in given path. Error message: $($_.Exception.Message)"; break + } + + # Check if given path contains any directories assuming they're exported administrative templates + try { + $AdministrativeTemplateFolders = Get-ChildItem -Path $Path -Directory -ErrorAction Stop + if ($AdministrativeTemplateFolders -eq $null) { + $SkipAdministrativeTemplateProfiles = $true + Write-Warning -Message "Specified path doesn't contain any exported Administrative Template folders, skipping administrative template profile import actions" + } + else { + Write-Verbose -Message "Specified path contains exported administrative template folders, will include those for import" + $SkipAdministrativeTemplateProfiles = $false + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to validate administrative template folders existence in given path. Error message: $($_.Exception.Message)"; break + } +} +Process { + # Functions + function Get-ErrorResponseBody { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Exception]$Exception + ) + + # Read the error stream + $ErrorResponseStream = $Exception.Response.GetResponseStream() + $StreamReader = New-Object System.IO.StreamReader($ErrorResponseStream) + $StreamReader.BaseStream.Position = 0 + $StreamReader.DiscardBufferedData() + $ResponseBody = $StreamReader.ReadToEnd() + + # Handle return object + return $ResponseBody + } + + function Invoke-IntuneGraphRequest { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$URI, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Object]$Body + ) + try { + # Call Graph API and get JSON response + $GraphResponse = Invoke-RestMethod -Uri $URI -Headers $AuthToken -Method Post -Body $Body -ContentType "application/json" -ErrorAction Stop -Verbose:$false + + return $GraphResponse + } + catch [System.Exception] { + # Construct stream reader for reading the response body from API call + $ResponseBody = Get-ErrorResponseBody -Exception $_.Exception + + # Handle response output and error message + Write-Output -InputObject "Response content:`n$ResponseBody" + Write-Warning -Message "Request to $($URI) failed with HTTP Status $($_.Exception.Response.StatusCode) and description: $($_.Exception.Response.StatusDescription)" + } + } + + function Test-JSON { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Object]$InputObject + ) + try { + # Convert from hash-table to JSON + ConvertTo-Json -InputObject $InputObject -ErrorAction Stop + + # Return true if conversion was successful + return $true + } + catch [System.Exception] { + return $false + } + } + + function Get-Platform { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$InputObject + ) + switch -Regex ($InputObject) { + "microsoft.graph.androidDeviceOwner" { + $PlatformType = "AndroidEnterprise" + } + "microsoft.graph.androidWorkProfile" { + $PlatformType = "AndroidEnterprise" + } + "microsoft.graph.windows" { + $PlatformType = "Windows" + } + "microsoft.graph.ios" { + $PlatformType = "iOS" + } + } + + # Handle return value + return $PlatformType + } + + function New-IntuneDeviceConfigurationProfile { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$JSON + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/deviceConfigurations" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + Invoke-IntuneGraphRequest -URI $GraphURI -Body $JSON + } + + function New-IntuneAdministrativeTemplateProfile { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$JSON + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/groupPolicyConfigurations" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + $GraphResponse = Invoke-IntuneGraphRequest -URI $GraphURI -Body $JSON + + # Handle return value + return $GraphResponse.id + } + + function New-IntuneAdministrativeTemplateDefinitionValues { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AdministrativeTemplateID, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$JSON + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/groupPolicyConfigurations/$($AdministrativeTemplateID)/definitionValues" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + Invoke-IntuneGraphRequest -URI $GraphURI -Body $JSON + } + + # Ensure all given platforms are available in a list array for further reference + $PlatformList = New-Object -TypeName System.Collections.ArrayList + foreach ($PlatformItem in $Platform) { + $PlatformList.Add($PlatformItem) | Out-Null + } + + # Process each JSON file located in given path + if ($SkipDeviceConfigurationProfiles -eq $false) { + foreach ($JSONFile in $JSONFiles.FullName) { + Write-Verbose -Message "Processing JSON data file from: $($JSONFile)" + + try { + # Read JSON data from current file + $JSONDataContent = Get-Content -Path $JSONFile -ErrorAction Stop -Verbose:$false + + try { + $JSONData = $JSONDataContent | ConvertFrom-Json -ErrorAction Stop | Select-Object -Property * -ExcludeProperty id, createdDateTime, lastModifiedDateTime, version, supportsScopeTags + $JSONPlatform = Get-Platform -InputObject $JSONData.'@odata.type' + $JSONDisplayName = $JSONData.displayName + + # Handle device configuration profile name if prefix parameter is specified + if ($PSBoundParameters["Prefix"]) { + $JSONDisplayName = -join($Prefix, $JSONData.displayName) + $JSONData.displayName = $JSONDisplayName + } + + if ($JSONPlatform -in $Platform) { + Write-Verbose -Message "Validating JSON data content import for profile: $($JSONDisplayName)" + + if (Test-JSON -InputObject $JSONData) { + Write-Verbose -Message "Successfully validated JSON data content for import, proceed to import profile" + + # Convert from object to JSON string + $JSONDataConvert = $JSONData | ConvertTo-Json -Depth 5 + + # Create new device configuration profile based on JSON data + Write-Verbose -Message "Attempting to create new device configuration profile with name: $($JSONDisplayName)" + $GraphRequest = New-IntuneDeviceConfigurationProfile -JSON $JSONDataConvert + + if ($GraphRequest.'@odata.type' -like $JSONPlatform) { + Write-Verbose -Message "Successfully created device configuration profile" + } + } + else { + Write-Verbose -Message "Failed to validate JSON data object to be converted to JSON string" + } + } + else { + Write-Verbose -Message "Current JSON data file for platform type '$($JSONPlatform)' was not allowed to be imported, skipping" + } + } + catch [System.Exception] { + Write-Warning -Message "Failed to convert JSON data content. Error message: $($_.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "Failed to read JSON data content from file '$($JSONFile)'. Error message: $($_.Exception.Message)" + } + } + } + else { + Write-Verbose -Message "Skipping device configuration profile import actions as no .json files was found in given location" + } + + # Process each administrative template folder + if ($SkipAdministrativeTemplateProfiles -eq $false) { + foreach ($AdministrativeTemplateFolder in $AdministrativeTemplateFolders) { + # Get administrative template variable parameters + $AdministrativeTemplateName = $AdministrativeTemplateFolder.Name + $AdministrativeTemplatePath = $AdministrativeTemplateFolder.FullName + + # Validate that current administrative template folder contains JSON files + $AdministrativeTemplateFolderJSONFiles = Get-ChildItem -Path $AdministrativeTemplatePath -Filter *.json + if ($AdministrativeTemplateFolderJSONFiles -ne $null) { + # Handle administrative template profile name if prefix parameter is specified + if ($PSBoundParameters["Prefix"]) { + $AdministrativeTemplateName = -join($Prefix, $AdministrativeTemplateName) + } + + # Construct new administrative template profile object + Write-Verbose -Message "Attempting to create new administrative template profile with name: $($AdministrativeTemplateName)" + $AdministrativeTemplateProfileJSONDataTable = @{ + "displayName" = $AdministrativeTemplateName + "description" = [string]::Empty + } + $AdministrativeTemplateProfileJSONData = $AdministrativeTemplateProfileJSONDataTable | ConvertTo-Json + $AdministrativeTemplateProfileID = New-IntuneAdministrativeTemplateProfile -JSON $AdministrativeTemplateProfileJSONData + + # Process each subsequent JSON file in current administrative template profile folder + foreach ($AdministrativeTemplateFolderJSONFile in $AdministrativeTemplateFolderJSONFiles) { + # Read JSON data from current file + $JSONDataContent = Get-Content -Path $AdministrativeTemplateFolderJSONFile.FullName -ErrorAction Stop -Verbose:$false + + try { + $JSONData = $JSONDataContent | ConvertFrom-Json -ErrorAction Stop + Write-Verbose -Message "Validating JSON data content import for defintion values: $($AdministrativeTemplateFolderJSONFile.Name)" + + if (Test-JSON -InputObject $JSONData) { + Write-Verbose -Message "Successfully validated JSON data content for import, proceed to import defintion values" + + # Convert from object to JSON string + $JSONDataConvert = $JSONData | ConvertTo-Json -Depth 5 + + # Create new administrative template definition values based on JSON data + Write-Verbose -Message "Attempting to create new administrative template definition values with name: $($AdministrativeTemplateFolderJSONFile.Name)" + $GraphRequest = New-IntuneAdministrativeTemplateDefinitionValues -AdministrativeTemplateID $AdministrativeTemplateProfileID -JSON $JSONDataConvert + + if ($GraphRequest.configurationType -like "policy") { + Write-Verbose -Message "Successfully created administrative template definition values" + } + } + else { + Write-Verbose -Message "Failed to validate JSON data object to be converted to JSON string" + } + } + catch [System.Exception] { + Write-Warning -Message "Failed to convert JSON data content from file. Error message: $($_.Exception.Message)" + } + } + } + else { + Write-Warning -Message "Failed to locate sub-sequent .json files within administrative template profile folder: $($AdministrativeTemplatePath)" + } + } + } + else { + Write-Verbose -Message "Skipping administrative template import actions as no sub-directories was found in given location" + } +} \ No newline at end of file diff --git a/Device Configuration/Rename-IntuneDeviceConfigurationProfile.ps1 b/Device Configuration/Rename-IntuneDeviceConfigurationProfile.ps1 new file mode 100644 index 0000000..dabc398 --- /dev/null +++ b/Device Configuration/Rename-IntuneDeviceConfigurationProfile.ps1 @@ -0,0 +1,251 @@ +<# +.SYNOPSIS + Rename a specified string (match pattern) in Device Configuration profile display names with a new string (replace pattern). + +.DESCRIPTION + Rename a specified string (match pattern) in Device Configuration profile display names with a new string (replace pattern). + +.PARAMETER TenantName + Specify the tenant name, e.g. domain.onmicrosoft.com. + +.PARAMETER Match + Specify the match pattern as a string that's represented in the device configuration profile name and will be updated with that's specified for the Replace parameter. + +.PARAMETER Replace + Specify the replace pattern as a string that will replace what's matched in the device configuration profile name. + +.PARAMETER ApplicationID + Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration. + +.PARAMETER PromptBehavior + Set the prompt behavior when acquiring a token. + +.EXAMPLE + # Rename all Device Configuration Profiles with a display name that matches 'Win10' with 'W10' in a tenant named 'domain.onmicrosoft.com': + .\Rename-IntuneDeviceConfigurationProfile.ps1 -TenantName "domain.onmicrosoft.com" -Match "Win10" -Replace "W10" -Verbose + +.NOTES + FileName: Rename-IntuneDeviceConfigurationProfile.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2019-10-15 + Updated: 2019-10-15 + + Version history: + 1.0.0 - (2019-10-15) Script created + + Required modules: + AzureAD (Install-Module -Name AzureAD) + PSIntuneAuth (Install-Module -Name PSIntuneAuth) +#> +[CmdletBinding(SupportsShouldProcess=$true)] +param( + [parameter(Mandatory = $true, HelpMessage = "Specify the tenant name, e.g. domain.onmicrosoft.com.")] + [ValidateNotNullOrEmpty()] + [string]$TenantName, + + [parameter(Mandatory = $true, HelpMessage = "Specify the match pattern as a string that's represented in the device configuration profile name and will be updated with that's specified for the Replace parameter.")] + [ValidateNotNullOrEmpty()] + [string]$Match, + + [parameter(Mandatory = $true, HelpMessage = "Specify the replace pattern as a string that will replace what's matched in the device configuration profile name.")] + [ValidateNotNullOrEmpty()] + [string]$Replace, + + [parameter(Mandatory = $false, HelpMessage = "Specify the Application ID of the app registration in Azure AD. By default, the script will attempt to use well known Microsoft Intune PowerShell app registration.")] + [ValidateNotNullOrEmpty()] + [string]$ApplicationID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547", + + [parameter(Mandatory=$false, HelpMessage="Set the prompt behavior when acquiring a token.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Auto", "Always", "Never", "RefreshSession")] + [string]$PromptBehavior = "Auto" +) +Begin { + # Determine if the PSIntuneAuth module needs to be installed + try { + Write-Verbose -Message "Attempting to locate PSIntuneAuth module" + $PSIntuneAuthModule = Get-InstalledModule -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false + if ($PSIntuneAuthModule -ne $null) { + Write-Verbose -Message "Authentication module detected, checking for latest version" + $LatestModuleVersion = (Find-Module -Name PSIntuneAuth -ErrorAction Stop -Verbose:$false).Version + if ($LatestModuleVersion -gt $PSIntuneAuthModule.Version) { + Write-Verbose -Message "Latest version of PSIntuneAuth module is not installed, attempting to install: $($LatestModuleVersion.ToString())" + $UpdateModuleInvocation = Update-Module -Name PSIntuneAuth -Scope CurrentUser -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + } + } + catch [System.Exception] { + Write-Warning -Message "Unable to detect PSIntuneAuth module, attempting to install from PSGallery" + try { + # Install NuGet package provider + $PackageProvider = Install-PackageProvider -Name NuGet -Force -Verbose:$false + + # Install PSIntuneAuth module + Install-Module -Name PSIntuneAuth -Scope AllUsers -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + Write-Verbose -Message "Successfully installed PSIntuneAuth" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to install PSIntuneAuth module. Error message: $($_.Exception.Message)"; break + } + } + + # Check if token has expired and if, request a new + Write-Verbose -Message "Checking for existing authentication token" + if ($Global:AuthToken -ne $null) { + $UTCDateTime = (Get-Date).ToUniversalTime() + $TokenExpireMins = ($Global:AuthToken.ExpiresOn.datetime - $UTCDateTime).Minutes + Write-Verbose -Message "Current authentication token expires in (minutes): $($TokenExpireMins)" + if ($TokenExpireMins -le 0) { + Write-Verbose -Message "Existing token found but has expired, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } + else { + if ($PromptBehavior -like "Always") { + Write-Verbose -Message "Existing authentication token has not expired but prompt behavior was set to always ask for authentication, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } + else { + Write-Verbose -Message "Existing authentication token has not expired, will not request a new token" + } + } + } + else { + Write-Verbose -Message "Authentication token does not exist, requesting a new token" + $Global:AuthToken = Get-MSIntuneAuthToken -TenantName $TenantName -ClientID $ApplicationID -PromptBehavior $PromptBehavior + } +} +Process { + # Functions + function Get-ErrorResponseBody { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Exception]$Exception + ) + + # Read the error stream + $ErrorResponseStream = $Exception.Response.GetResponseStream() + $StreamReader = New-Object System.IO.StreamReader($ErrorResponseStream) + $StreamReader.BaseStream.Position = 0 + $StreamReader.DiscardBufferedData() + $ResponseBody = $StreamReader.ReadToEnd() + + # Handle return object + return $ResponseBody + } + + function Invoke-IntuneGraphRequest { + param( + [parameter(Mandatory = $true, ParameterSetName = "Get")] + [parameter(ParameterSetName = "Patch")] + [ValidateNotNullOrEmpty()] + [string]$URI, + + [parameter(Mandatory = $true, ParameterSetName = "Patch")] + [ValidateNotNullOrEmpty()] + [System.Object]$Body + ) + try { + # Construct array list for return values + $ResponseList = New-Object -TypeName System.Collections.ArrayList + + # Call Graph API and get JSON response + switch ($PSCmdlet.ParameterSetName) { + "Get" { + Write-Verbose -Message "Current Graph API call is using method: Get" + $GraphResponse = Invoke-RestMethod -Uri $URI -Headers $AuthToken -Method Get -ErrorAction Stop -Verbose:$false + if ($GraphResponse -ne $null) { + if ($GraphResponse.value -ne $null) { + foreach ($Response in $GraphResponse.value) { + $ResponseList.Add($Response) | Out-Null + } + } + else { + $ResponseList.Add($GraphResponse) | Out-Null + } + } + } + "Patch" { + Write-Verbose -Message "Current Graph API call is using method: Patch" + $GraphResponse = Invoke-RestMethod -Uri $URI -Headers $AuthToken -Method Patch -Body $Body -ContentType "application/json" -ErrorAction Stop -Verbose:$false + if ($GraphResponse -ne $null) { + foreach ($ResponseItem in $GraphResponse) { + $ResponseList.Add($ResponseItem) | Out-Null + } + } + else { + Write-Warning -Message "Response was null..." + } + } + } + + return $ResponseList + } + catch [System.Exception] { + # Construct stream reader for reading the response body from API call + $ResponseBody = Get-ErrorResponseBody -Exception $_.Exception + + # Handle response output and error message + Write-Output -InputObject "Response content:`n$ResponseBody" + Write-Warning -Message "Request to $($URI) failed with HTTP Status $($_.Exception.Response.StatusCode) and description: $($_.Exception.Response.StatusDescription)" + } + } + + function Get-IntuneDeviceConfigurationProfile { + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/deviceConfigurations" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + $GraphResponse = Invoke-IntuneGraphRequest -URI $GraphURI + + # Handle return objects from response + return $GraphResponse + } + + function Set-IntuneDeviceConfigurationProfileDisplayName { + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$DeviceConfigurationProfileID, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Object]$Body + ) + # Construct Graph variables + $GraphVersion = "beta" + $GraphResource = "deviceManagement/deviceConfigurations/$($DeviceConfigurationProfileID)" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + + # Invoke Graph API resource call + $GraphResponse = Invoke-IntuneGraphRequest -URI $GraphURI -Body $Body + } + + # Get all device configuration profiles and process each object + $DeviceConfigurationProfiles = Get-IntuneDeviceConfigurationProfile + if ($DeviceConfigurationProfiles -ne $null) { + foreach ($DeviceConfigurationProfile in $DeviceConfigurationProfiles) { + Write-Verbose -Message "Processing current device configuration profile with name: $($DeviceConfigurationProfile.displayName)" + + if ($DeviceConfigurationProfile.displayName -match $Match) { + Write-Verbose -Message "Match found for current device configuration profile, will attempt to rename object" + + # Construct JSON object for POST call + $NewName = $DeviceConfigurationProfile.displayName.Replace($Match, $Replace) + $JSONTable = @{ + '@odata.type' = $DeviceConfigurationProfile.'@odata.type' + 'id' = $DeviceConfigurationProfile.id + 'displayName' = $NewName + } + $JSONData = $JSONTable | ConvertTo-Json + + # Call Graph API post operation with new display name + Write-Verbose -Message "Attempting to rename '$($DeviceConfigurationProfile.displayName)' profile to: $($NewName)" + Set-IntuneDeviceConfigurationProfileDisplayName -DeviceConfigurationProfileID $DeviceConfigurationProfile.id -Body $JSONData + } + } + } +} \ No newline at end of file diff --git a/Drivers/Invoke-HPDriverUpdate.ps1 b/Drivers/Invoke-HPDriverUpdate.ps1 new file mode 100644 index 0000000..37e9492 --- /dev/null +++ b/Drivers/Invoke-HPDriverUpdate.ps1 @@ -0,0 +1,414 @@ +<# +.SYNOPSIS + Download and install the latest set of drivers and driver software from HP repository online using HP Image Assistant for current client device. + +.DESCRIPTION + This script will download and install the latest matching drivers and driver software from HP repository online using HP Image Assistant that will + analyze what's required for the current client device it's running on. + +.PARAMETER RunMode + Select run mode for this script, either Stage or Execute. + +.PARAMETER HPIAAction + Specify the HP Image Assistant action to perform, e.g. Download or Install. + +.EXAMPLE + .\Invoke-HPDriverUpdate.ps1 -RunMode "Stage" + +.NOTES + FileName: Invoke-HPDriverUpdate.ps1 + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-08-12 + Updated: 2021-04-07 + + Version history: + 1.0.0 - (2020-08-12) Script created + 1.0.1 - (2020-09-15) Added a fix for registering default PSGallery repository if not already registered + 1.0.2 - (2020-09-28) Added a new parameter HPIAAction that controls whether to Download or Install applicable drivers + 1.0.3 - (2021-04-07) Replaced Get-Softpaq cmdlet with a hard-coded softpaq number with the newly added Install-HPImageAssistant cmdlet in the HPCMSL module +#> +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [parameter(Mandatory = $true, HelpMessage = "Select run mode for this script, either Stage or Execute.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Stage", "Execute")] + [string]$RunMode, + + [parameter(Mandatory = $false, HelpMessage = "Specify the HP Image Assistant action to perform, e.g. Download or Install.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Download", "Install")] + [string]$HPIAAction = "Install" +) +Begin { + # Enable TLS 1.2 support for downloading modules from PSGallery + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +} +Process { + # Functions + function Write-LogEntry { + param ( + [parameter(Mandatory=$true, HelpMessage="Value added to the log file.")] + [ValidateNotNullOrEmpty()] + [string]$Value, + + [parameter(Mandatory=$true, HelpMessage="Severity for the log entry. 1 for Informational, 2 for Warning and 3 for Error.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("1", "2", "3")] + [string]$Severity, + + [parameter(Mandatory=$false, HelpMessage="Name of the log file that the entry will written to.")] + [ValidateNotNullOrEmpty()] + [string]$FileName = "HPDriverUpdate.log" + ) + # Determine log file location + $WindowsTempLocation = (Join-Path -Path $env:windir -ChildPath "Temp") + $LogFilePath = Join-Path -Path $WindowsTempLocation -ChildPath $FileName + + # Construct time stamp for log entry + $Time = -join @((Get-Date -Format "HH:mm:ss.fff"), "+", (Get-WmiObject -Class Win32_TimeZone | Select-Object -ExpandProperty Bias)) + + # Construct date for log entry + $Date = (Get-Date -Format "MM-dd-yyyy") + + # Construct context for log entry + $Context = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) + + # Construct final log entry + $LogText = "" + + # Add value to log file and if specified console output + try { + if ($Script:PSBoundParameters["Verbose"]) { + # Write either verbose or warning output to console + switch ($Severity) { + 1 { + Write-Verbose -Message $Value + } + default { + Write-Warning -Message $Value + } + } + } + + # Write output to log file + Out-File -InputObject $LogText -Append -NoClobber -Encoding Default -FilePath $LogFilePath -ErrorAction Stop + } + catch [System.Exception] { + Write-Warning -Message "Unable to append log entry to HPDriverUpdate.log file. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)" + } + } + + function Set-RegistryValue { + param( + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Path, + + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + + [parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$Value + ) + try { + $RegistryValue = Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue + if ($RegistryValue -ne $null) { + Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force -ErrorAction Stop + } + else { + if (-not(Test-Path -Path $Path)) { + New-Item -Path $Path -ItemType Directory -Force -ErrorAction Stop | Out-Null + } + New-ItemProperty -Path $Path -Name $Name -PropertyType String -Value $Value -Force -ErrorAction Stop + } + } + catch [System.Exception] { + Write-Warning -Message "Failed to create or update registry value '$($Name)' in '$($Path)'. Error message: $($_.Exception.Message)" + } + } + + function Invoke-Executable { + param ( + [parameter(Mandatory = $true, HelpMessage = "Specify the file name or path of the executable to be invoked, including the extension.")] + [ValidateNotNullOrEmpty()] + [string]$FilePath, + + [parameter(Mandatory = $false, HelpMessage = "Specify arguments that will be passed to the executable.")] + [ValidateNotNull()] + [string]$Arguments + ) + + # Construct a hash-table for default parameter splatting + $SplatArgs = @{ + FilePath = $FilePath + NoNewWindow = $true + Passthru = $true + ErrorAction = "Stop" + } + + # Add ArgumentList param if present + if (-not ([System.String]::IsNullOrEmpty($Arguments))) { + $SplatArgs.Add("ArgumentList", $Arguments) + } + + # Invoke executable and wait for process to exit + try { + $Invocation = Start-Process @SplatArgs + $Handle = $Invocation.Handle + $Invocation.WaitForExit() + } + catch [System.Exception] { + Write-Warning -Message $_.Exception.Message; break + } + + # Handle return value with exitcode from process + return $Invocation.ExitCode + } + + function Start-PowerShellSysNative { + param ( + [parameter(Mandatory = $false, HelpMessage = "Specify arguments that will be passed to the sysnative PowerShell process.")] + [ValidateNotNull()] + [string]$Arguments + ) + + # Get the sysnative path for powershell.exe + $SysNativePowerShell = Join-Path -Path ($PSHOME.ToLower().Replace("syswow64", "sysnative")) -ChildPath "powershell.exe" + + # Construct new ProcessStartInfo object to restart powershell.exe as a 64-bit process and re-run scipt + $ProcessStartInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo + $ProcessStartInfo.FileName = $SysNativePowerShell + $ProcessStartInfo.Arguments = $Arguments + $ProcessStartInfo.RedirectStandardOutput = $true + $ProcessStartInfo.RedirectStandardError = $true + $ProcessStartInfo.UseShellExecute = $false + $ProcessStartInfo.WindowStyle = "Hidden" + $ProcessStartInfo.CreateNoWindow = $true + + # Instatiate the new 64-bit process + $Process = [System.Diagnostics.Process]::Start($ProcessStartInfo) + + # Read standard error output to determine if the 64-bit script process somehow failed + $ErrorOutput = $Process.StandardError.ReadToEnd() + if ($ErrorOutput) { + Write-Error -Message $ErrorOutput + } + } + + # Stage script in system root directory for ActiveSetup + $WindowsTempPath = Join-Path -Path $env:SystemRoot -ChildPath "Temp" + if (-not(Test-Path -Path (Join-Path -Path $WindowsTempPath -ChildPath $MyInvocation.MyCommand.Name))) { + Write-LogEntry -Value "Attempting to stage '$($MyInvocation.MyCommand.Definition)' to: $($WindowsTempPath)" -Severity 1 + Copy-Item $MyInvocation.MyCommand.Definition -Destination $WindowsTempPath -Force + } + else { + Write-LogEntry -Value "Found existing script file '$($MyInvocation.MyCommand.Definition)' in '$($WindowsTempPath)', will not attempt to stage again" -Severity 1 + } + + # Check if we're running as a 64-bit process or not, if not restart as a 64-bit process + if (-not[System.Environment]::Is64BitProcess) { + Write-LogEntry -Value "Re-launching the PowerShell instance as a 64-bit process in Stage mode since it was originally launched as a 32-bit process" -Severity 1 + Start-PowerShellSysNative -Arguments "-ExecutionPolicy Bypass -File $($env:SystemRoot)\Temp\$($MyInvocation.MyCommand.Name) -RunMode Stage" + } + else { + # Validate that script is executed on HP hardware + $Manufacturer = (Get-WmiObject -Class "Win32_ComputerSystem" | Select-Object -ExpandProperty Manufacturer).Trim() + switch -Wildcard ($Manufacturer) { + "*HP*" { + Write-LogEntry -Value "Validated HP hardware check, allowed to continue" -Severity 1 + } + "*Hewlett-Packard*" { + Write-LogEntry -Value "Validated HP hardware check, allowed to continue" -Severity 1 + } + default { + Write-LogEntry -Value "Unsupported hardware detected, HP hardware is required for this script to operate" -Severity 3; exit 1 + } + } + + switch ($RunMode) { + "Stage" { + Write-LogEntry -Value "Current script host process is running in 64-bit: $([System.Environment]::Is64BitProcess)" -Severity 1 + + try { + # Install latest NuGet package provider + Write-LogEntry -Value "Attempting to install latest NuGet package provider" -Severity 1 + $PackageProvider = Install-PackageProvider -Name "NuGet" -Force -ErrorAction Stop -Verbose:$false + + # Ensure default PSGallery repository is registered + Register-PSRepository -Default -ErrorAction SilentlyContinue + + # Attempt to get the installed PowerShellGet module + Write-LogEntry -Value "Attempting to locate installed PowerShellGet module" -Severity 1 + $PowerShellGetInstalledModule = Get-InstalledModule -Name "PowerShellGet" -ErrorAction SilentlyContinue -Verbose:$false + if ($PowerShellGetInstalledModule -ne $null) { + try { + # Attempt to locate the latest available version of the PowerShellGet module from repository + Write-LogEntry -Value "Attempting to request the latest PowerShellGet module version from repository" -Severity 1 + $PowerShellGetLatestModule = Find-Module -Name "PowerShellGet" -ErrorAction Stop -Verbose:$false + if ($PowerShellGetLatestModule -ne $null) { + if ($PowerShellGetInstalledModule.Version -lt $PowerShellGetLatestModule.Version) { + try { + # Newer module detected, attempt to update + Write-LogEntry -Value "Newer version detected, attempting to update the PowerShellGet module from repository" -Severity 1 + Update-Module -Name "PowerShellGet" -Scope "AllUsers" -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to update the PowerShellGet module. Error message: $($_.Exception.Message)" -Severity 3 ; exit 1 + } + } + } + else { + Write-LogEntry -Value "Location request for the latest available version of the PowerShellGet module failed, can't continue" -Severity 3; exit 1 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to retrieve the latest available version of the PowerShellGet module, can't continue. Error message: $($_.Exception.Message)" -Severity 3; exit 1 + } + } + else { + try { + # PowerShellGet module was not found, attempt to install from repository + Write-LogEntry -Value "PowerShellGet module was not found, will attempting to install it and it's dependencies from repository" -Severity 1 + Write-LogEntry -Value "Attempting to install PackageManagement module from repository" -Severity 1 + Install-Module -Name "PackageManagement" -Force -Scope AllUsers -AllowClobber -ErrorAction Stop -Verbose:$false + Write-LogEntry -Value "Attempting to install PowerShellGet module from repository" -Severity 1 + Install-Module -Name "PowerShellGet" -Force -Scope AllUsers -AllowClobber -ErrorAction Stop -Verbose:$false + } + catch [System.Exception] { + Write-LogEntry -Value "Unable to install PowerShellGet module from repository. Error message: $($_.Exception.Message)" -Severity 3; exit 1 + } + } + + try { + # Invoke executing script again in Execute run mode after package provider and modules have been installed/updated + Write-LogEntry -Value "Re-launching the PowerShell instance in Execute mode to overcome a bug with PowerShellGet" -Severity 1 + Start-PowerShellSysNative -Arguments "-ExecutionPolicy Bypass -File $($env:SystemRoot)\Temp\$($MyInvocation.MyCommand.Name) -RunMode Execute" + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to restart executing script in Execute run mode. Error message: $($_.Exception.Message)" -Severity 3; exit 1 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Unable to install latest NuGet package provider. Error message: $($_.Exception.Message)" -Severity 3; exit 1 + } + } + "Execute" { + try { + # Install HP Client Management Script Library + Write-LogEntry -Value "Attempting to install HPCMSL module from repository" -Severity 1 + Install-Module -Name "HPCMSL" -AcceptLicense -Force -ErrorAction Stop -Verbose:$false + + # Create HPIA directory for HP Image Assistant extraction + $HPImageAssistantExtractPath = Join-Path -Path $env:SystemRoot -ChildPath "Temp\HPIA" + if (-not(Test-Path -Path $HPImageAssistantExtractPath)) { + Write-LogEntry -Value "Creating directory for HP Image Assistant extraction: $($HPImageAssistantExtractPath)" -Severity 1 + New-Item -Path $HPImageAssistantExtractPath -ItemType "Directory" -Force | Out-Null + } + + # Create HP logs for HP Image Assistant + $HPImageAssistantReportPath = Join-Path -Path $env:SystemRoot -ChildPath "Temp\HPIALogs" + if (-not(Test-Path -Path $HPImageAssistantReportPath)) { + Write-LogEntry -Value "Creating directory for HP Image Assistant report logs: $($HPImageAssistantReportPath)" -Severity 1 + New-Item -Path $HPImageAssistantReportPath -ItemType "Directory" -Force | Out-Null + } + + # Create HP Drivers directory for driver content + $SoftpaqDownloadPath = Join-Path -Path $env:SystemRoot -ChildPath "Temp\HPDrivers" + if (-not(Test-Path -Path $SoftpaqDownloadPath)) { + Write-LogEntry -Value "Creating directory for softpaq downloads: $($SoftpaqDownloadPath)" -Severity 1 + New-Item -Path $SoftpaqDownloadPath -ItemType "Directory" -Force | Out-Null + } + + # Set current working directory to HPIA directory + Write-LogEntry -Value "Switching working directory to: $($env:SystemRoot)\Temp" -Severity 1 + Set-Location -Path (Join-Path -Path $env:SystemRoot -ChildPath "Temp") + + try { + # Download HP Image Assistant softpaq and extract it to Temp directory + Write-LogEntry -Value "Attempting to download and extract HP Image Assistant to: $($HPImageAssistantExtractPath)" -Severity 1 + Install-HPImageAssistant -Extract -DestinationPath $HPImageAssistantExtractPath -Quiet -ErrorAction Stop + + try { + # Invoke HP Image Assistant to install drivers and driver software + $HPImageAssistantExecutablePath = Join-Path -Path $env:SystemRoot -ChildPath "Temp\HPIA\HPImageAssistant.exe" + switch ($HPIAAction) { + "Download" { + Write-LogEntry -Value "Attempting to execute HP Image Assistant to download drivers including driver software, this might take some time" -Severity 1 + + # Prepare arguments for HP Image Assistant download mode + $HPImageAssistantArguments = "/Operation:Analyze /Action:Download /Selection:All /Silent /Category:Drivers,Software /ReportFolder:$($HPImageAssistantReportPath) /SoftpaqDownloadFolder:$($SoftpaqDownloadPath)" + + # Set HP Image Assistant operational mode in registry + Set-RegistryValue -Path "HKLM:\SOFTWARE\HP\ImageAssistant" -Name "OperationalMode" -Value "Download" -ErrorAction Stop + } + "Install" { + Write-LogEntry -Value "Attempting to execute HP Image Assistant to download and install drivers including driver software, this might take some time" -Severity 1 + + # Prepare arguments for HP Image Assistant install mode + $HPImageAssistantArguments = "/Operation:Analyze /Action:Install /Selection:All /Silent /Category:Drivers,Software /ReportFolder:$($HPImageAssistantReportPath) /SoftpaqDownloadFolder:$($SoftpaqDownloadPath)" + + # Set HP Image Assistant operational mode in registry + Set-RegistryValue -Path "HKLM:\SOFTWARE\HP\ImageAssistant" -Name "OperationalMode" -Value "Install" -ErrorAction Stop + } + } + + # Invoke HP Image Assistant + $Invocation = Invoke-Executable -FilePath $HPImageAssistantExecutablePath -Arguments $HPImageAssistantArguments -ErrorAction Stop + + # Add a registry key for Win32 app detection rule based on HP Image Assistant exit code + switch ($Invocation) { + 0 { + Write-LogEntry -Value "HP Image Assistant returned successful exit code: $($Invocation)" -Severity 1 + Set-RegistryValue -Path "HKLM:\SOFTWARE\HP\ImageAssistant" -Name "ExecutionResult" -Value "Success" -ErrorAction Stop + } + 256 { # The analysis returned no recommendations + Write-LogEntry -Value "HP Image Assistant returned there were no recommendations for this system, exit code: $($Invocation)" -Severity 1 + Set-RegistryValue -Path "HKLM:\SOFTWARE\HP\ImageAssistant" -Name "ExecutionResult" -Value "Success" -ErrorAction Stop + } + 3010 { # Softpaqs installations are successful, but at least one requires a restart + Write-LogEntry -Value "HP Image Assistant returned successful exit code: $($Invocation)" -Severity 1 + Set-RegistryValue -Path "HKLM:\SOFTWARE\HP\ImageAssistant" -Name "ExecutionResult" -Value "Success" -ErrorAction Stop + } + 3020 { # One or more Softpaq's failed to install + Write-LogEntry -Value "HP Image Assistant did not install one or more softpaqs successfully, examine the Readme*.html file in: $($HPImageAssistantReportPath)" -Severity 2 + Write-LogEntry -Value "HP Image Assistant returned successful exit code: $($Invocation)" -Severity 1 + Set-RegistryValue -Path "HKLM:\SOFTWARE\HP\ImageAssistant" -Name "ExecutionResult" -Value "Success" -ErrorAction Stop + } + default { + Write-LogEntry -Value "HP Image Assistant returned unhandled exit code: $($Invocation)" -Severity 3 + Set-RegistryValue -Path "HKLM:\SOFTWARE\HP\ImageAssistant" -Name "ExecutionResult" -Value "Failed" -ErrorAction Stop + } + } + + if ($HPIAAction -like "Install") { + # Cleanup downloaded softpaq executable that was extracted + Write-LogEntry -Value "Attempting to cleanup directory for downloaded softpaqs: $($SoftpaqDownloadPath)" -Severity 1 + Remove-Item -Path $SoftpaqDownloadPath -Force -Recurse -Confirm:$false + } + + # Cleanup extracted HPIA directory + Write-LogEntry -Value "Attempting to cleanup extracted HP Image Assistant directory: $($HPImageAssistantExtractPath)" -Severity 1 + Remove-Item -Path $HPImageAssistantExtractPath -Force -Recurse -Confirm:$false + + # Remove script from Temp directory + Write-LogEntry -Value "Attempting to self-destruct executing script file: $($MyInvocation.MyCommand.Definition)" -Severity 1 + Remove-Item -Path $MyInvocation.MyCommand.Definition -Force -Confirm:$false + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to run HP Image Assistant to install drivers and driver software. Error message: $($_.Exception.Message)" -Severity 3; exit 1 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Failed to download and extract HP Image Assistant softpaq. Error message: $($_.Exception.Message)" -Severity 3; exit 1 + } + } + catch [System.Exception] { + Write-LogEntry -Value "Unable to install HPCMSL module from repository. Error message: $($_.Exception.Message)" -Severity 3; exit 1 + } + } + } + } +} \ No newline at end of file diff --git a/Drivers/Invoke-MSIntuneDriverUpdate.ps1 b/Drivers/Invoke-MSIntuneDriverUpdate.ps1 new file mode 100644 index 0000000..1bd067b --- /dev/null +++ b/Drivers/Invoke-MSIntuneDriverUpdate.ps1 @@ -0,0 +1,753 @@ +<# +.SYNOPSIS + + The purpose of this script is to automate the driver update process when enrolling devices through + Microsoft Intune. + +.DESCRIPTION + + This script will determine the model of the computer, manufacturer and operating system used then download, + extract & install the latest driver package from the manufacturer. At present Dell, HP and Lenovo devices + are supported. + +.NOTES + + FileName: Invoke-MSIntuneDriverUpdate.ps1 + + Author: Maurice Daly + Contact: @MoDaly_IT + Created: 2017-12-03 + Updated: 2017-12-05 + + Version history: + + 1.0.0 - (2017-12-03) Script created + 1.0.1 - (2017-12-05) Updated Lenovo matching SKU value and added regex matching for Computer Model values. + 1.0.2 - (2017-12-05) Updated to cater for language differences in OS architecture returned +#> + +# // =================== GLOBAL VARIABLES ====================== // + +$TempLocation = Join-Path $env:SystemDrive "Temp\SCConfigMgr" + +# Set Temp & Log Location +[string]$TempDirectory = Join-Path $TempLocation "\Temp" +[string]$LogDirectory = Join-Path $TempLocation "\Logs" + +# Create Temp Folder +if ((Test-Path -Path $TempDirectory) -eq $false) { + New-Item -Path $TempDirectory -ItemType Dir +} + +# Create Logs Folder +if ((Test-Path -Path $LogDirectory) -eq $false) { + New-Item -Path $LogDirectory -ItemType Dir +} + +# Logging Function +function global:Write-CMLogEntry { + param ( + [parameter(Mandatory = $true, HelpMessage = "Value added to the log file.")] + [ValidateNotNullOrEmpty()] + [string] + $Value, + [parameter(Mandatory = $true, HelpMessage = "Severity for the log entry. 1 for Informational, 2 for Warning and 3 for Error.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("1", "2", "3")] + [string] + $Severity, + [parameter(Mandatory = $false, HelpMessage = "Name of the log file that the entry will written to.")] + [ValidateNotNullOrEmpty()] + [string] + $FileName = "Invoke-MSIntuneDriverUpdate.log" + ) + # Determine log file location + $LogFilePath = Join-Path -Path $LogDirectory -ChildPath $FileName + # Construct time stamp for log entry + $Time = -join @((Get-Date -Format "HH:mm:ss.fff"), "+", (Get-WmiObject -Class Win32_TimeZone | Select-Object -ExpandProperty Bias)) + # Construct date for log entry + $Date = (Get-Date -Format "MM-dd-yyyy") + # Construct context for log entry + $Context = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) + # Construct final log entry + $LogText = "" + # Add value to log file + try { + Add-Content -Value $LogText -LiteralPath $LogFilePath -ErrorAction Stop + } + catch [System.Exception] { + Write-Warning -Message "Unable to append log entry to Invoke-DriverUpdate.log file. Error message: $($_.Exception.Message)" + } +} + +# // =================== DELL VARIABLES ================ // + +# Define Dell Download Sources +$DellDownloadList = "http://downloads.dell.com/published/Pages/index.html" +$DellDownloadBase = "http://downloads.dell.com" +$DellDriverListURL = "http://en.community.dell.com/techcenter/enterprise-client/w/wiki/2065.dell-command-deploy-driver-packs-for-enterprise-client-os-deployment" +$DellBaseURL = "http://en.community.dell.com" + +# Define Dell Download Sources +$DellXMLCabinetSource = "http://downloads.dell.com/catalog/DriverPackCatalog.cab" +$DellCatalogSource = "http://downloads.dell.com/catalog/CatalogPC.cab" + +# Define Dell Cabinet/XL Names and Paths +$DellCabFile = [string]($DellXMLCabinetSource | Split-Path -Leaf) +$DellCatalogFile = [string]($DellCatalogSource | Split-Path -Leaf) +$DellXMLFile = $DellCabFile.Trim(".cab") +$DellXMLFile = $DellXMLFile + ".xml" +$DellCatalogXMLFile = $DellCatalogFile.Trim(".cab") + ".xml" + +# Define Dell Global Variables +$DellCatalogXML = $null +$DellModelXML = $null +$DellModelCabFiles = $null + +# // =================== HP VARIABLES ================ // + +# Define HP Download Sources +$HPXMLCabinetSource = "http://ftp.hp.com/pub/caps-softpaq/cmit/HPClientDriverPackCatalog.cab" +$HPSoftPaqSource = "http://ftp.hp.com/pub/softpaq/" +$HPPlatFormList = "http://ftp.hp.com/pub/caps-softpaq/cmit/imagepal/ref/platformList.cab" + +# Define HP Cabinet/XL Names and Paths +$HPCabFile = [string]($HPXMLCabinetSource | Split-Path -Leaf) +$HPXMLFile = $HPCabFile.Trim(".cab") +$HPXMLFile = $HPXMLFile + ".xml" +$HPPlatformCabFile = [string]($HPPlatFormList | Split-Path -Leaf) +$HPPlatformXMLFile = $HPPlatformCabFile.Trim(".cab") +$HPPlatformXMLFile = $HPPlatformXMLFile + ".xml" + +# Define HP Global Variables +$global:HPModelSoftPaqs = $null +$global:HPModelXML = $null +$global:HPPlatformXML = $null + +# // =================== LENOVO VARIABLES ================ // + +# Define Lenovo Download Sources +$global:LenovoXMLSource = "https://download.lenovo.com/cdrt/td/catalog.xml" + +# Define Lenovo Cabinet/XL Names and Paths +$global:LenovoXMLFile = [string]($global:LenovoXMLSource | Split-Path -Leaf) + +# Define Lenovo Global Variables +$global:LenovoModelDrivers = $null +$global:LenovoModelXML = $null +$global:LenovoModelType = $null +$global:LenovoSystemSKU = $null + +# // =================== COMMON VARIABLES ================ // + +# Determine manufacturer +$ComputerManufacturer = (Get-WmiObject -Class Win32_ComputerSystem | Select-Object -ExpandProperty Manufacturer).Trim() +Write-CMLogEntry -Value "Manufacturer determined as: $($ComputerManufacturer)" -Severity 1 + +# Determine manufacturer name and hardware information +switch -Wildcard ($ComputerManufacturer) { + "*HP*" { + $ComputerManufacturer = "Hewlett-Packard" + $ComputerModel = Get-WmiObject -Class Win32_ComputerSystem | Select-Object -ExpandProperty Model + $SystemSKU = (Get-CIMInstance -ClassName MS_SystemInformation -NameSpace root\WMI).BaseBoardProduct + } + "*Hewlett-Packard*" { + $ComputerManufacturer = "Hewlett-Packard" + $ComputerModel = Get-WmiObject -Class Win32_ComputerSystem | Select-Object -ExpandProperty Model + $SystemSKU = (Get-CIMInstance -ClassName MS_SystemInformation -NameSpace root\WMI).BaseBoardProduct + } + "*Dell*" { + $ComputerManufacturer = "Dell" + $ComputerModel = Get-WmiObject -Class Win32_ComputerSystem | Select-Object -ExpandProperty Model + $SystemSKU = (Get-CIMInstance -ClassName MS_SystemInformation -NameSpace root\WMI).SystemSku + } + "*Lenovo*" { + $ComputerManufacturer = "Lenovo" + $ComputerModel = Get-WmiObject -Class Win32_ComputerSystemProduct | Select-Object -ExpandProperty Version + $SystemSKU = ((Get-CIMInstance -ClassName MS_SystemInformation -NameSpace root\WMI | Select-Object -ExpandProperty BIOSVersion).SubString(0, 4)).Trim() + } +} +Write-CMLogEntry -Value "Computer model determined as: $($ComputerModel)" -Severity 1 + +if (-not [string]::IsNullOrEmpty($SystemSKU)) { + Write-CMLogEntry -Value "Computer SKU determined as: $($SystemSKU)" -Severity 1 +} + +# Get operating system name from version +switch -wildcard (Get-WmiObject -Class Win32_OperatingSystem | Select-Object -ExpandProperty Version) { + "10.0*" { + $OSName = "Windows 10" + } + "6.3*" { + $OSName = "Windows 8.1" + } + "6.1*" { + $OSName = "Windows 7" + } +} +Write-CMLogEntry -Value "Operating system determined as: $OSName" -Severity 1 + +# Get operating system architecture +switch -wildcard ((Get-CimInstance Win32_operatingsystem).OSArchitecture) { + "64-*" { + $OSArchitecture = "64-Bit" + } + "32-*" { + $OSArchitecture = "32-Bit" + } +} + +Write-CMLogEntry -Value "Architecture determined as: $OSArchitecture" -Severity 1 + +$WindowsVersion = ($OSName).Split(" ")[1] + +function DownloadDriverList { + global:Write-CMLogEntry -Value "======== Download Model Link Information ========" -Severity 1 + if ($ComputerManufacturer -eq "Hewlett-Packard") { + if ((Test-Path -Path $TempDirectory\$HPCabFile) -eq $false) { + global:Write-CMLogEntry -Value "======== Downloading HP Product List ========" -Severity 1 + # Download HP Model Cabinet File + global:Write-CMLogEntry -Value "Info: Downloading HP driver pack cabinet file from $HPXMLCabinetSource" -Severity 1 + try { + Start-BitsTransfer -Source $HPXMLCabinetSource -Destination $TempDirectory + # Expand Cabinet File + global:Write-CMLogEntry -Value "Info: Expanding HP driver pack cabinet file: $HPXMLFile" -Severity 1 + Expand "$TempDirectory\$HPCabFile" -F:* "$TempDirectory\$HPXMLFile" + } + catch { + global:Write-CMLogEntry -Value "Error: $($_.Exception.Message)" -Severity 3 + } + } + # Read XML File + if ($global:HPModelSoftPaqs -eq $null) { + global:Write-CMLogEntry -Value "Info: Reading driver pack XML file - $TempDirectory\$HPXMLFile" -Severity 1 + [xml]$global:HPModelXML = Get-Content -Path $TempDirectory\$HPXMLFile + # Set XML Object + $global:HPModelXML.GetType().FullName | Out-Null + $global:HPModelSoftPaqs = $HPModelXML.NewDataSet.HPClientDriverPackCatalog.ProductOSDriverPackList.ProductOSDriverPack + } + } + if ($ComputerManufacturer -eq "Dell") { + if ((Test-Path -Path $TempDirectory\$DellCabFile) -eq $false) { + global:Write-CMLogEntry -Value "Info: Downloading Dell product list" -Severity 1 + global:Write-CMLogEntry -Value "Info: Downloading Dell driver pack cabinet file from $DellXMLCabinetSource" -Severity 1 + # Download Dell Model Cabinet File + try { + Start-BitsTransfer -Source $DellXMLCabinetSource -Destination $TempDirectory + # Expand Cabinet File + global:Write-CMLogEntry -Value "Info: Expanding Dell driver pack cabinet file: $DellXMLFile" -Severity 1 + Expand "$TempDirectory\$DellCabFile" -F:* "$TempDirectory\$DellXMLFile" + } + catch { + global:Write-CMLogEntry -Value "Error: $($_.Exception.Message)" -Severity 3 + } + } + if ($DellModelXML -eq $null) { + # Read XML File + global:Write-CMLogEntry -Value "Info: Reading driver pack XML file - $TempDirectory\$DellXMLFile" -Severity 1 + [xml]$DellModelXML = (Get-Content -Path $TempDirectory\$DellXMLFile) + # Set XML Object + $DellModelXML.GetType().FullName | Out-Null + } + $DellModelCabFiles = $DellModelXML.driverpackmanifest.driverpackage + + } + if ($ComputerManufacturer -eq "Lenovo") { + if ($global:LenovoModelDrivers -eq $null) { + try { + [xml]$global:LenovoModelXML = Invoke-WebRequest -Uri $global:LenovoXMLSource + } + catch { + global:Write-CMLogEntry -Value "Error: $($_.Exception.Message)" -Severity 3 + } + + # Read Web Site + global:Write-CMLogEntry -Value "Info: Reading driver pack URL - $global:LenovoXMLSource" -Severity 1 + + # Set XML Object + $global:LenovoModelXML.GetType().FullName | Out-Null + $global:LenovoModelDrivers = $global:LenovoModelXML.Products + } + } +} + +function FindLenovoDriver { + +<# + # This powershell file will extract the link for the specified driver pack or application + # param $URI The string version of the URL + # param $64bit A boolean to determine what version to pick if there are multiple + # param $os A string containing 7, 8, or 10 depending on the os we are deploying + # i.e. 7, Win7, Windows 7 etc are all valid os strings + #> + param ( + [parameter(Mandatory = $true, HelpMessage = "Provide the URL to parse.")] + [ValidateNotNullOrEmpty()] + [string] + $URI, + [parameter(Mandatory = $true, HelpMessage = "Specify the operating system.")] + [ValidateNotNullOrEmpty()] + [string] + $OS, + [string] + $Architecture + ) + + #Case for direct link to a zip file + if ($URI.EndsWith(".zip")) { + return $URI + } + + $err = @() + + #Get the content of the website + try { + $html = Invoke-WebRequest –Uri $URI + } + catch { + global:Write-CMLogEntry -Value "Error: $($_.Exception.Message)" -Severity 3 + } + + #Create an array to hold all the links to exe files + $Links = @() + $Links.Clear() + + #determine if the URL resolves to the old download location + if ($URI -like "*olddownloads*") { + #Quickly grab the links that end with exe + $Links = (($html.Links | Where-Object { + $_.href -like "*exe" + }) | Where class -eq "downloadBtn").href + } + + $Links = ((Select-string '(http[s]?)(:\/\/)([^\s,]+.exe)(?=")' -InputObject ($html).Rawcontent -AllMatches).Matches.Value) + + if ($Links.Count -eq 0) { + return $null + } + + # Switch OS architecture + switch -wildcard ($Architecture) { + "*64*" { + $Architecture = "64" + } + "*86*" { + $Architecture = "32" + } + } + + #if there are multiple links then narrow down to the proper arc and os (if needed) + if ($Links.Count -gt 0) { + #Second array of links to hold only the ones we want to target + $MatchingLink = @() + $MatchingLink.clear() + foreach ($Link in $Links) { + if ($Link -like "*w$($OS)$($Architecture)_*" -or $Link -like "*w$($OS)_$($Architecture)*") { + $MatchingLink += $Link + } + } + } + + if ($MatchingLink -ne $null) { + return $MatchingLink + } + else { + return "badLink" + } +} + +function Get-RedirectedUrl { + Param ( + [Parameter(Mandatory = $true)] + [String] + $URL + ) + + $Request = [System.Net.WebRequest]::Create($URL) + $Request.AllowAutoRedirect = $false + $Request.Timeout = 3000 + $Response = $Request.GetResponse() + + if ($Response.ResponseUri) { + $Response.GetResponseHeader("Location") + } + $Response.Close() +} + +function LenovoModelTypeFinder { + param ( + [parameter(Mandatory = $false, HelpMessage = "Enter Lenovo model to query")] + [string] + $ComputerModel, + [parameter(Mandatory = $false, HelpMessage = "Enter Operating System")] + [string] + $OS, + [parameter(Mandatory = $false, HelpMessage = "Enter Lenovo model type to query")] + [string] + $ComputerModelType + ) + try { + if ($global:LenovoModelDrivers -eq $null) { + [xml]$global:LenovoModelXML = Invoke-WebRequest -Uri $global:LenovoXMLSource + # Read Web Site + global:Write-CMLogEntry -Value "Info: Reading driver pack URL - $global:LenovoXMLSource" -Severity 1 + + # Set XML Object + $global:LenovoModelXML.GetType().FullName | Out-Null + $global:LenovoModelDrivers = $global:LenovoModelXML.Products + } + } + catch { + global:Write-CMLogEntry -Value "Error: $($_.Exception.Message)" -Severity 3 + } + + if ($ComputerModel.Length -gt 0) { + $global:LenovoModelType = ($global:LenovoModelDrivers.Product | Where-Object { + $_.Queries.Version -match "$ComputerModel" + }).Queries.Types | Select -ExpandProperty Type | Select -first 1 + $global:LenovoSystemSKU = ($global:LenovoModelDrivers.Product | Where-Object { + $_.Queries.Version -match "$ComputerModel" + }).Queries.Types | select -ExpandProperty Type | Get-Unique + } + + if ($ComputerModelType.Length -gt 0) { + $global:LenovoModelType = (($global:LenovoModelDrivers.Product.Queries) | Where-Object { + ($_.Types | Select -ExpandProperty Type) -match $ComputerModelType + }).Version | Select -first 1 + } + Return $global:LenovoModelType +} + +function InitiateDownloads { + + $Product = "Intune Driver Automation" + + # Driver Download ScriptBlock + $DriverDownloadJob = { + Param ([string] + $TempDirectory, + [string] + $ComputerModel, + [string] + $DriverCab, + [string] + $DriverDownloadURL + ) + + try { + # Start Driver Download + Start-BitsTransfer -DisplayName "$ComputerModel-DriverDownload" -Source $DriverDownloadURL -Destination "$($TempDirectory + '\Driver Cab\' + $DriverCab)" + } + catch [System.Exception] { + global:Write-CMLogEntry -Value "Error: $($_.Exception.Message)" -Severity 3 + } + } + + global:Write-CMLogEntry -Value "======== Starting Download Processes ========" -Severity 1 + global:Write-CMLogEntry -Value "Info: Operating System specified: Windows $($WindowsVersion)" -Severity 1 + global:Write-CMLogEntry -Value "Info: Operating System architecture specified: $($OSArchitecture)" -Severity 1 + + # Operating System Version + $OperatingSystem = ("Windows " + $($WindowsVersion)) + + # Vendor Make + $ComputerModel = $ComputerModel.Trim() + + # Get Windows Version Number + switch -Wildcard ((Get-WmiObject -Class Win32_OperatingSystem).Version) { + "*10.0.16*" { + $OSBuild = "1709" + } + "*10.0.15*" { + $OSBuild = "1703" + } + "*10.0.14*" { + $OSBuild = "1607" + } + } + global:Write-CMLogEntry -Value "Info: Windows 10 build $OSBuild identified for driver match" -Severity 1 + + # Start driver import processes + global:Write-CMLogEntry -Value "Info: Starting Download,Extract And Import Processes For $ComputerManufacturer Model: $($ComputerModel)" -Severity 1 + + # =================== DEFINE VARIABLES ===================== + + if ($ComputerManufacturer -eq "Dell") { + global:Write-CMLogEntry -Value "Info: Setting Dell variables" -Severity 1 + if ($DellModelCabFiles -eq $null) { + [xml]$DellModelXML = Get-Content -Path $TempDirectory\$DellXMLFile + # Set XML Object + $DellModelXML.GetType().FullName | Out-Null + $DellModelCabFiles = $DellModelXML.driverpackmanifest.driverpackage + } + if ($SystemSKU -ne $null) { + global:Write-CMLogEntry -Value "Info: SystemSKU value is present, attempting match based on SKU - $SystemSKU)" -Severity 1 + + $ComputerModelURL = $DellDownloadBase + "/" + ($DellModelCabFiles | Where-Object { + ((($_.SupportedOperatingSystems).OperatingSystem).osCode -like "*$WindowsVersion*") -and ($_.SupportedSystems.Brand.Model.SystemID -eq $SystemSKU) + }).delta + $ComputerModelURL = $ComputerModelURL.Replace("\", "/") + $DriverDownload = $DellDownloadBase + "/" + ($DellModelCabFiles | Where-Object { + ((($_.SupportedOperatingSystems).OperatingSystem).osCode -like "*$WindowsVersion*") -and ($_.SupportedSystems.Brand.Model.SystemID -eq $SystemSKU) + }).path + $DriverCab = (($DellModelCabFiles | Where-Object { + ((($_.SupportedOperatingSystems).OperatingSystem).osCode -like "*$WindowsVersion*") -and ($_.SupportedSystems.Brand.Model.SystemID -eq $SystemSKU) + }).path).Split("/") | select -Last 1 + } + elseif ($SystemSKU -eq $null -or $DriverCab -eq $null) { + global:Write-CMLogEntry -Value "Info: Falling back to matching based on model name" -Severity 1 + + $ComputerModelURL = $DellDownloadBase + "/" + ($DellModelCabFiles | Where-Object { + ((($_.SupportedOperatingSystems).OperatingSystem).osCode -like "*$WindowsVersion*") -and ($_.SupportedSystems.Brand.Model.Name -like "*$ComputerModel*") + }).delta + $ComputerModelURL = $ComputerModelURL.Replace("\", "/") + $DriverDownload = $DellDownloadBase + "/" + ($DellModelCabFiles | Where-Object { + ((($_.SupportedOperatingSystems).OperatingSystem).osCode -like "*$WindowsVersion*") -and ($_.SupportedSystems.Brand.Model.Name -like "*$ComputerModel") + }).path + $DriverCab = (($DellModelCabFiles | Where-Object { + ((($_.SupportedOperatingSystems).OperatingSystem).osCode -like "*$WindowsVersion*") -and ($_.SupportedSystems.Brand.Model.Name -like "*$ComputerModel") + }).path).Split("/") | select -Last 1 + } + $DriverRevision = (($DriverCab).Split("-")[2]).Trim(".cab") + $DellSystemSKU = ($DellModelCabFiles.supportedsystems.brand.model | Where-Object { + $_.Name -match ("^" + $ComputerModel + "$") + } | Get-Unique).systemID + if ($DellSystemSKU.count -gt 1) { + $DellSystemSKU = [string]($DellSystemSKU -join ";") + } + global:Write-CMLogEntry -Value "Info: Dell System Model ID is : $DellSystemSKU" -Severity 1 + } + if ($ComputerManufacturer -eq "Hewlett-Packard") { + global:Write-CMLogEntry -Value "Info: Setting HP variables" -Severity 1 + if ($global:HPModelSoftPaqs -eq $null) { + [xml]$global:HPModelXML = Get-Content -Path $TempDirectory\$HPXMLFile + # Set XML Object + $global:HPModelXML.GetType().FullName | Out-Null + $global:HPModelSoftPaqs = $global:HPModelXML.NewDataSet.HPClientDriverPackCatalog.ProductOSDriverPackList.ProductOSDriverPack + } + if ($SystemSKU -ne $null) { + $HPSoftPaqSummary = $global:HPModelSoftPaqs | Where-Object { + ($_.SystemID -match $SystemSKU) -and ($_.OSName -like "$OSName*$OSArchitecture*$OSBuild*") + } | Sort-Object -Descending | select -First 1 + } + else { + $HPSoftPaqSummary = $global:HPModelSoftPaqs | Where-Object { + ($_.SystemName -match $ComputerModel) -and ($_.OSName -like "$OSName*$OSArchitecture*$OSBuild*") + } | Sort-Object -Descending | select -First 1 + } + if ($HPSoftPaqSummary -ne $null) { + $HPSoftPaq = $HPSoftPaqSummary.SoftPaqID + $HPSoftPaqDetails = $global:HPModelXML.newdataset.hpclientdriverpackcatalog.softpaqlist.softpaq | Where-Object { + $_.ID -eq "$HPSoftPaq" + } + $ComputerModelURL = $HPSoftPaqDetails.URL + # Replace FTP for HTTP for Bits Transfer Job + $DriverDownload = ($HPSoftPaqDetails.URL).TrimStart("ftp:") + $DriverCab = $ComputerModelURL | Split-Path -Leaf + $DriverRevision = "$($HPSoftPaqDetails.Version)" + } + else{ + Write-CMLogEntry -Value "Unsupported model / operating system combination found. Exiting." -Severity 3; exit 1 + } + } + if ($ComputerManufacturer -eq "Lenovo") { + global:Write-CMLogEntry -Value "Info: Setting Lenovo variables" -Severity 1 + $global:LenovoModelType = LenovoModelTypeFinder -ComputerModel $ComputerModel -OS $WindowsVersion + global:Write-CMLogEntry -Value "Info: $ComputerManufacturer $ComputerModel matching model type: $global:LenovoModelType" -Severity 1 + + if ($global:LenovoModelDrivers -ne $null) { + [xml]$global:LenovoModelXML = (New-Object System.Net.WebClient).DownloadString("$global:LenovoXMLSource") + # Set XML Object + $global:LenovoModelXML.GetType().FullName | Out-Null + $global:LenovoModelDrivers = $global:LenovoModelXML.Products + if ($SystemSKU -ne $null) { + $ComputerModelURL = (($global:LenovoModelDrivers.Product | Where-Object { + ($_.Queries.smbios -match $SystemSKU -and $_.OS -match $WindowsVersion) + }).driverPack | Where-Object { + $_.id -eq "SCCM" + })."#text" + } + else { + $ComputerModelURL = (($global:LenovoModelDrivers.Product | Where-Object { + ($_.Queries.Version -match ("^" + $ComputerModel + "$") -and $_.OS -match $WindowsVersion) + }).driverPack | Where-Object { + $_.id -eq "SCCM" + })."#text" + } + global:Write-CMLogEntry -Value "Info: Model URL determined as $ComputerModelURL" -Severity 1 + $DriverDownload = FindLenovoDriver -URI $ComputerModelURL -os $WindowsVersion -Architecture $OSArchitecture + If ($DriverDownload -ne $null) { + $DriverCab = $DriverDownload | Split-Path -Leaf + $DriverRevision = ($DriverCab.Split("_") | Select -Last 1).Trim(".exe") + global:Write-CMLogEntry -Value "Info: Driver cabinet download determined as $DriverDownload" -Severity 1 + } + else { + global:Write-CMLogEntry -Value "Error: Unable to find driver for $Make $Model" -Severity 1 + } + } + } + + # Driver location variables + $DriverSourceCab = ($TempDirectory + "\Driver Cab\" + $DriverCab) + $DriverExtractDest = ("$TempDirectory" + "\Driver Files") + global:Write-CMLogEntry -Value "Info: Driver extract location set - $DriverExtractDest" -Severity 1 + + # =================== INITIATE DOWNLOADS =================== + + global:Write-CMLogEntry -Value "======== $Product - $ComputerManufacturer $ComputerModel DRIVER PROCESSING STARTED ========" -Severity 1 + + # =============== ConfigMgr Driver Cab Download ================= + global:Write-CMLogEntry -Value "$($Product): Retrieving ConfigMgr driver pack site For $ComputerManufacturer $ComputerModel" -Severity 1 + global:Write-CMLogEntry -Value "$($Product): URL found: $ComputerModelURL" -Severity 1 + + if (($ComputerModelURL -ne $null) -and ($DriverDownload -ne "badLink")) { + # Cater for HP / Model Issue + $ComputerModel = $ComputerModel -replace '/', '-' + $ComputerModel = $ComputerModel.Trim() + Set-Location -Path $TempDirectory + # Check for destination directory, create if required and download the driver cab + if ((Test-Path -Path $($TempDirectory + "\Driver Cab\" + $DriverCab)) -eq $false) { + if ((Test-Path -Path $($TempDirectory + "\Driver Cab")) -eq $false) { + New-Item -ItemType Directory -Path $($TempDirectory + "\Driver Cab") + } + global:Write-CMLogEntry -Value "$($Product): Downloading $DriverCab driver cab file" -Severity 1 + global:Write-CMLogEntry -Value "$($Product): Downloading from URL: $DriverDownload" -Severity 1 + Start-Job -Name "$ComputerModel-DriverDownload" -ScriptBlock $DriverDownloadJob -ArgumentList ($TempDirectory, $ComputerModel, $DriverCab, $DriverDownload) + sleep -Seconds 5 + $BitsJob = Get-BitsTransfer | Where-Object { + $_.DisplayName -match "$ComputerModel-DriverDownload" + } + while (($BitsJob).JobState -eq "Connecting") { + global:Write-CMLogEntry -Value "$($Product): Establishing connection to $DriverDownload" -Severity 1 + sleep -seconds 30 + } + while (($BitsJob).JobState -eq "Transferring") { + if ($BitsJob.BytesTotal -ne $null) { + $PercentComplete = [int](($BitsJob.BytesTransferred * 100)/$BitsJob.BytesTotal); + global:Write-CMLogEntry -Value "$($Product): Downloaded $([int]((($BitsJob).BytesTransferred)/ 1MB)) MB of $([int]((($BitsJob).BytesTotal)/ 1MB)) MB ($PercentComplete%). Next update in 30 seconds." -Severity 1 + sleep -seconds 30 + } + else { + global:Write-CMLogEntry -Value "$($Product): Download issues detected. Cancelling download process" -Severity 2 + Get-BitsTransfer | Where-Object { + $_.DisplayName -eq "$ComputerModel-DriverDownload" + } | Remove-BitsTransfer + } + } + Get-BitsTransfer | Where-Object { + $_.DisplayName -eq "$ComputerModel-DriverDownload" + } | Complete-BitsTransfer + global:Write-CMLogEntry -Value "$($Product): Driver revision: $DriverRevision" -Severity 1 + } + else { + global:Write-CMLogEntry -Value "$($Product): Skipping $DriverCab. Driver pack already downloaded." -Severity 1 + } + + # Cater for HP / Model Issue + $ComputerModel = $ComputerModel -replace '/', '-' + + if (((Test-Path -Path "$($TempDirectory + '\Driver Cab\' + $DriverCab)") -eq $true) -and ($DriverCab -ne $null)) { + global:Write-CMLogEntry -Value "$($Product): $DriverCab File exists - Starting driver update process" -Severity 1 + # =============== Extract Drivers ================= + + if ((Test-Path -Path "$DriverExtractDest") -eq $false) { + New-Item -ItemType Directory -Path "$($DriverExtractDest)" + } + if ((Get-ChildItem -Path "$DriverExtractDest" -Recurse -Filter *.inf -File).Count -eq 0) { + global:Write-CMLogEntry -Value "==================== $PRODUCT DRIVER EXTRACT ====================" -Severity 1 + global:Write-CMLogEntry -Value "$($Product): Expanding driver CAB source file: $DriverCab" -Severity 1 + global:Write-CMLogEntry -Value "$($Product): Driver CAB destination directory: $DriverExtractDest" -Severity 1 + if ($ComputerManufacturer -eq "Dell") { + global:Write-CMLogEntry -Value "$($Product): Extracting $ComputerManufacturer drivers to $DriverExtractDest" -Severity 1 + Expand "$DriverSourceCab" -F:* "$DriverExtractDest" + } + if ($ComputerManufacturer -eq "Hewlett-Packard") { + global:Write-CMLogEntry -Value "$($Product): Extracting $ComputerManufacturer drivers to $HPTemp" -Severity 1 + # Driver Silent Extract Switches + $HPSilentSwitches = "-PDF -F" + "$DriverExtractDest" + " -S -E" + global:Write-CMLogEntry -Value "$($Product): Using $ComputerManufacturer silent switches: $HPSilentSwitches" -Severity 1 + Start-Process -FilePath "$($TempDirectory + '\Driver Cab\' + $DriverCab)" -ArgumentList $HPSilentSwitches -Verb RunAs + $DriverProcess = ($DriverCab).Substring(0, $DriverCab.length - 4) + + # Wait for HP SoftPaq Process To Finish + While ((Get-Process).name -contains $DriverProcess) { + global:Write-CMLogEntry -Value "$($Product): Waiting for extract process (Process: $DriverProcess) to complete.. Next check in 30 seconds" -Severity 1 + sleep -Seconds 30 + } + } + if ($ComputerManufacturer -eq "Lenovo") { + # Driver Silent Extract Switches + $global:LenovoSilentSwitches = "/VERYSILENT /DIR=" + '"' + $DriverExtractDest + '"' + ' /Extract="Yes"' + global:Write-CMLogEntry -Value "$($Product): Using $ComputerManufacturer silent switches: $global:LenovoSilentSwitches" -Severity 1 + global:Write-CMLogEntry -Value "$($Product): Extracting $ComputerManufacturer drivers to $DriverExtractDest" -Severity 1 + Unblock-File -Path $($TempDirectory + '\Driver Cab\' + $DriverCab) + Start-Process -FilePath "$($TempDirectory + '\Driver Cab\' + $DriverCab)" -ArgumentList $global:LenovoSilentSwitches -Verb RunAs + $DriverProcess = ($DriverCab).Substring(0, $DriverCab.length - 4) + # Wait for Lenovo Driver Process To Finish + While ((Get-Process).name -contains $DriverProcess) { + global:Write-CMLogEntry -Value "$($Product): Waiting for extract process (Process: $DriverProcess) to complete.. Next check in 30 seconds" -Severity 1 + sleep -seconds 30 + } + } + } + else { + global:Write-CMLogEntry -Value "Skipping. Drivers already extracted." -Severity 1 + } + } + else { + global:Write-CMLogEntry -Value "$($Product): $DriverCab file download failed" -Severity 3 + } + } + elseif ($DriverDownload -eq "badLink") { + global:Write-CMLogEntry -Value "$($Product): Operating system driver package download path not found.. Skipping $ComputerModel" -Severity 3 + } + else { + global:Write-CMLogEntry -Value "$($Product): Driver package not found for $ComputerModel running Windows $WindowsVersion $Architecture. Skipping $ComputerModel" -Severity 2 + } + global:Write-CMLogEntry -Value "======== $PRODUCT - $ComputerManufacturer $ComputerModel DRIVER PROCESSING FINISHED ========" -Severity 1 + + + if ($ValidationErrors -eq 0) { + + } +} + +function Update-Drivers { + $DriverPackagePath = Join-Path $TempDirectory "Driver Files" + Write-CMLogEntry -Value "Driver package location is $DriverPackagePath" -Severity 1 + Write-CMLogEntry -Value "Starting driver installation process" -Severity 1 + Write-CMLogEntry -Value "Reading drivers from $DriverPackagePath" -Severity 1 + # Apply driver maintenance package + try { + if ((Get-ChildItem -Path $DriverPackagePath -Filter *.inf -Recurse).count -gt 0) { + try { + Start-Process "$env:WINDIR\sysnative\windowspowershell\v1.0\powershell.exe" -WorkingDirectory $DriverPackagePath -ArgumentList "pnputil /add-driver *.inf /subdirs /install | Out-File -FilePath (Join-Path $LogDirectory '\Install-Drivers.txt') -Append" -NoNewWindow -Wait + Write-CMLogEntry -Value "Driver installation complete. Restart required" -Severity 1 + } + catch [System.Exception] + { + Write-CMLogEntry -Value "An error occurred while attempting to apply the driver maintenance package. Error message: $($_.Exception.Message)" -Severity 3; exit 1 + } + } + else { + Write-CMLogEntry -Value "No driver inf files found in $DriverPackagePath." -Severity 3; exit 1 + } + } + catch [System.Exception] { + Write-CMLogEntry -Value "An error occurred while attempting to apply the driver maintenance package. Error message: $($_.Exception.Message)" -Severity 3; exit 1 + } + Write-CMLogEntry -Value "Finished driver maintenance." -Severity 1 + Return $LastExitCode +} + +if ($OSName -eq "Windows 10") { + # Download manufacturer lists for driver matching + DownloadDriverList + # Initiate matched downloads + InitiateDownloads + # Update driver repository and install drivers + Update-Drivers +} +else { + Write-CMLogEntry -Value "An upsupported OS was detected. This script only supports Windows 10." -Severity 3; exit 1 +} diff --git a/IntuneDelayedTargeting/Invoke-DelayedTargetingGroup.ps1 b/IntuneDelayedTargeting/Invoke-DelayedTargetingGroup.ps1 new file mode 100644 index 0000000..1a2beec --- /dev/null +++ b/IntuneDelayedTargeting/Invoke-DelayedTargetingGroup.ps1 @@ -0,0 +1,65 @@ +<# + .SYNOPSIS + Automated script for delayed targeting based on a set timerange. + .DESCRIPTION + Add devices to group X hours after enrolled to Intune to avoid certain scripts and packages to be targeted during provisioning. + .PARAMETERS + TargetingGroupID: The ObjectID of the group you are maintainging in Azure AD + .NOTES + Author: Jan Ketil Skanke + Contact: @JankeSkanke + Created: 2021-06-14 + Updated: 2021-06-14 + Version history: + 1.0.0 - (2021-09-22 ) Production Ready version +#> + +function Get-MSIAccessTokenGraph{ + $resourceURL = "https://graph.microsoft.com/" + $response = [System.Text.Encoding]::Default.GetString((Invoke-WebRequest -UseBasicParsing -Uri "$($env:IDENTITY_ENDPOINT)?resource=$resourceURL" -Method 'GET' -Headers @{'X-IDENTITY-HEADER' = "$env:IDENTITY_HEADER"; 'Metadata' = 'True'}).RawContentStream.ToArray()) | ConvertFrom-Json + $accessToken = $response.access_token + + #Create Global Authentication Header + $Global:AuthenticationHeader = @{ + "Content-Type" = "application/json" + "Authorization" = "Bearer " + $accessToken + } +return $AuthenticationHeader +} + +$TargetingGroupID = "" # Or use a Automation Account Variable +#Connect to AzAccount for AZOperations +$Connecting = Connect-AzAccount -Identity + +#Connect to Graph for Graph Operations +$Response = Get-MSIAccessTokenGraph + +#Set timeslot to check for new devices to add +$starttime = Get-Date((Get-Date).AddHours(-28)) -Format "yyyy-MM-ddTHH:mm:ssZ" +$endtime = Get-Date((Get-Date).AddHours(-4)) -Format "yyyy-MM-ddTHH:mm:ssZ" + +# Fetch all newly deployed MTRs and process them (Change filter if your are not using for MTRs) +$Devices = Invoke-MSGraphOperation -APIVersion Beta -Get -Resource "deviceManagement/manageddevices?filter=startswith(deviceName, 'MTR-') and ((enrolleddatetime+lt+$($endtime)) and (enrolleddatetime+gt+$($starttime)))" +#This line below can be used for the first run to add all targeted devices up until -x hours. +#$Devices = Invoke-MSGraphOperation -APIVersion Beta -Get -Resource "deviceManagement/manageddevices?filter=startswith(deviceName, 'MTR-') and (enrolleddatetime+lt+$($endtime))" + +# Fetch all devices currently in group +$DeviceIDsInGroup = (Get-AzADGroupMember -GroupObjectId $TargetingGroupID).Id +if (-not([string]::IsNullOrEmpty($Devices))){ + foreach($device in $devices){ + $DeviceID = $device.azureADDeviceId + $DirectoryObjectID = (Invoke-MSGraphOperation -APIVersion Beta -Get -Resource "devices?filter=deviceId+eq+`'$DeviceID`'").id + if (-not ($DirectoryObjectID -in $DeviceIDsInGroup)){ + try { + Add-AzADGroupMember -MemberObjectId $DirectoryObjectID -TargetGroupObjectId $TargetingGroupID -ErrorAction Stop + Write-Output "Added $($device.deviceName) with ID $($DirectoryObjectID) to group" + } catch { + Write-Output "Failed to add $($device.deviceName) to group. Message: $($_.Exception.Message)" + } + } else { + Write-Output "$($device.deviceName) with ID $($DirectoryObjectID) already in group" + } + } +} else { + Write-Output "No new devices to process this time, exiting script" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e6f45a4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 MSEndpointMgr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Modules/PSIntuneAuth/PSIntuneAuth.psd1 b/Modules/PSIntuneAuth/PSIntuneAuth.psd1 index 8c9ea04..7198b66 100644 --- a/Modules/PSIntuneAuth/PSIntuneAuth.psd1 +++ b/Modules/PSIntuneAuth/PSIntuneAuth.psd1 @@ -7,12 +7,11 @@ # @{ - # Script module or binary module file associated with this manifest. RootModule = 'PSIntuneAuth.psm1' # Version number of this module. -ModuleVersion = '1.0.2' +ModuleVersion = '1.2.3' # Supported PSEditions # CompatiblePSEditions = @() @@ -27,10 +26,10 @@ Author = 'Nickolaj Andersen' CompanyName = 'SCConfigMgr.com' # Copyright statement for this module -Copyright = '(c) 2017 Nickolaj Andersen. All rights reserved.' +Copyright = '(c) 2020 Nickolaj Andersen. All rights reserved.' # Description of the functionality provided by this module -Description = 'Provides a function to retrieve an authentication token for Intune Graph API automation.' +Description = 'Provides a function to retrieve an authentication token for Intune Graph API calls.' # Minimum version of the Windows PowerShell engine required by this module PowerShellVersion = '4.0' @@ -69,7 +68,7 @@ PowerShellVersion = '4.0' # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = 'Get-MSIntuneAuthToken' +FunctionsToExport = 'Get-MSIntuneAuthToken', 'Set-MSIntuneAdminConsent' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() @@ -101,7 +100,7 @@ PrivateData = @{ # LicenseUri = '' # A URL to the main website for this project. - ProjectUri = 'https://github.com/SCConfigMgr/Intune/tree/master/Modules/PSIntuneAuth' + ProjectUri = 'https://github.com/MSEndpointMgr/Intune/tree/master/Modules/PSIntuneAuth' # A URL to an icon representing this module. # IconUri = '' diff --git a/Modules/PSIntuneAuth/PSIntuneAuth.psm1 b/Modules/PSIntuneAuth/PSIntuneAuth.psm1 index 3545c7a..560d40c 100644 --- a/Modules/PSIntuneAuth/PSIntuneAuth.psm1 +++ b/Modules/PSIntuneAuth/PSIntuneAuth.psm1 @@ -8,24 +8,289 @@ function Get-MSIntuneAuthToken { A tenant name should be provided in the following format: tenantname.onmicrosoft.com. .PARAMETER ClientID - Application ID for an Azure AD application. + Application ID for an Azure AD application. Uses by default the Microsoft Intune PowerShell application ID. + + .PARAMETER ClientSecret + Web application client secret. + + .PARAMETER Credential + Specify a PSCredential object containing username and password. + + .PARAMETER Resource + Resource recipient (app, e.g. Graph API). Leave empty to use https://graph.microsoft.com as default. .PARAMETER RedirectUri Redirect URI for Azure AD application. Leave empty to leverage Azure PowerShell well known redirect URI. + .PARAMETER PromptBehavior + Set the prompt behavior when acquiring a token. + .EXAMPLE - Get-MSGraphAuthenticationToken -TenantName domain.onmicrsoft.com -ClientID "" + # Manually specify username and password to acquire an authentication token: + Get-MSIntuneAuthToken -TenantName domain.onmicrsoft.com + + # Manually specify username and password to acquire an authentication token using a specific client ID: + Get-MSIntuneAuthToken -TenantName domain.onmicrsoft.com -ClientID "" + + # Retrieve a PSCredential object with username and password to acquire an authentication token: + $Credential = Get-Credential + Get-MSIntuneAuthToken -TenantName domain.onmicrsoft.com -Credential $Credential + + # Retrieve a PSCredential object for usage with Azure Automation containing the username and password to acquire an authentication token: + $Credential = Get-AutomationPSCredential -Name "" + Get-MSIntuneAuthToken -TenantName domain.onmicrsoft.com -ClientID "" -Credential $Credential + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2017-09-27 + Updated: 2021-01-12 + + Version history: + 1.0.0 - (2017-09-27) Function created + 1.0.1 - (2017-10-08) Added ExpiresOn property + 1.0.2 - (2018-01-22) Added support for specifying PSCredential object for silently retrieving an authentication token without being prompted + 1.0.3 - (2018-01-22) Fixed an issue with prompt behavior parameter not being used + 1.0.4 - (2018-01-22) Fixed an issue when detecting the AzureAD module presence + 1.0.5 - (2018-01-22) Enhanced the AzureAD module detection logic + 1.0.6 - (2018-01-28) Changed so that the Microsoft Intune PowerShell application ID is set as default for ClientID parameter + 1.2.0 - (2019-10-27) Added support for using app-only authentication using a client ID and client secret for a web app. Resource recipient is now also possible + to specify directly on the command line instead of being hard-coded. Now using the latest authority URI and installs the AzureAD module automatically. + 1.2.1 - (2020-01-15) Fixed an issue where when multiple versions of the AzureAD module installed would cause an error attempting in re-installing the Azure AD module + 1.2.2 - (2020-01-28) Added more verbose logging output for further troubleshooting in case an auth token is not aquired + 1.2.3 - (2021-01-12) Added support for installing the AzureAD module along side with the AzureADPreview module + #> + [CmdletBinding()] + param( + [parameter(Mandatory=$true, ParameterSetName="AuthPrompt", HelpMessage="A tenant name should be provided in the following format: tenantname.onmicrosoft.com.")] + [parameter(Mandatory=$true, ParameterSetName="AuthCredential")] + [parameter(Mandatory=$false, ParameterSetName="AuthAppOnly")] + [ValidateNotNullOrEmpty()] + [string]$TenantName, + + [parameter(Mandatory=$false, ParameterSetName="AuthPrompt", HelpMessage="Application ID for an Azure AD application. Uses by default the Microsoft Intune PowerShell application ID.")] + [parameter(Mandatory=$false, ParameterSetName="AuthCredential")] + [parameter(Mandatory=$false, ParameterSetName="AuthAppOnly")] + [ValidateNotNullOrEmpty()] + [string]$ClientID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547", + + [parameter(Mandatory=$true, ParameterSetName="AuthAppOnly", HelpMessage="Web application client secret.")] + [ValidateNotNullOrEmpty()] + [string]$ClientSecret, + + [parameter(Mandatory=$true, ParameterSetName="AuthCredential", HelpMessage="Specify a PSCredential object containing username and password.")] + [ValidateNotNullOrEmpty()] + [PSCredential]$Credential, + + [parameter(Mandatory=$false, ParameterSetName="AuthPrompt", HelpMessage="Resource recipient (app, e.g. Graph API). Leave empty to use https://graph.microsoft.com as default.")] + [parameter(Mandatory=$false, ParameterSetName="AuthCredential")] + [parameter(Mandatory=$false, ParameterSetName="AuthAppOnly")] + [ValidateNotNullOrEmpty()] + [string]$Resource = "https://graph.microsoft.com", + + [parameter(Mandatory=$false, ParameterSetName="AuthPrompt", HelpMessage="Redirect URI for Azure AD application. Leave empty to leverage Azure PowerShell well known redirect URI.")] + [parameter(Mandatory=$false, ParameterSetName="AuthCredential")] + [ValidateNotNullOrEmpty()] + [string]$RedirectUri = "urn:ietf:wg:oauth:2.0:oob", + + [parameter(Mandatory=$false, ParameterSetName="AuthPrompt", HelpMessage="Set the prompt behavior when acquiring a token.")] + [parameter(Mandatory=$false, ParameterSetName="AuthCredential")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Auto", "Always", "Never", "RefreshSession")] + [string]$PromptBehavior = "Auto" + ) + Process { + $ErrorActionPreference = "Stop" + + # Determine if the AzureAD module needs to be installed or updated to latest version + try { + Write-Verbose -Message "Attempting to locate AzureAD module on local system" + $AzureADModule = Get-Module -Name "AzureAD" -ListAvailable -Verbose:$false + if ($AzureADModule -ne $null) { + if (($AzureADModule | Measure-Object).Count -eq 1) { + $CurrentModuleVersion = Get-Module -Name "AzureAD" -ListAvailable -ErrorAction Stop -Verbose:$false | Select-Object -ExpandProperty Version + } + else { + $CurrentModuleVersion = Get-Module -Name "AzureAD" -ListAvailable -ErrorAction Stop -Verbose:$false | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version + } + $LatestModuleVersion = (Find-Module -Name "AzureAD" -ErrorAction Stop -Verbose:$false).Version + Write-Verbose -Message "AzureAD module detected, checking for latest version" + if ($LatestModuleVersion -gt $CurrentModuleVersion) { + Write-Verbose -Message "Latest version of AzureAD module is not installed, attempting to install: $($LatestModuleVersion.ToString())" + $UpdateModuleInvocation = Update-Module -Name "AzureAD" -Scope "AllUsers" -Force -ErrorAction Stop -Confirm:$false -Verbose:$false + } + else { + Write-Verbose -Message "Latest version for AzureAD module was detected, continue to aquire authentication token" + } + } + else { + throw "Unable to detect Azure AD module" + } + } + catch [System.Exception] { + Write-Warning -Message "Unable to detect AzureAD module, attempting to install from online repository" + try { + # Install NuGet package provider + $PackageProvider = Install-PackageProvider -Name NuGet -Force -Verbose:$false + + # Install AzureAD module + $InstallArgs = @{ + "Name" = "AzureAD" + "Scope" = "AllUsers" + "Force" = $true + "ErrorAction" = "Stop" + "Confirm" = $false + "Verbose" = $false + } + + # Amend install args if AzureADPreview module is detected + $AzureADPreviewModule = Get-Module -Name "AzureADPreview" -ListAvailable -Verbose:$false + if ($AzureADPreviewModule -ne $null) { + Write-Verbose -Message "Detected that the AzureADPreview module was installed, adding 'AllowClobber' parameter" + $InstallArgs.Add("AllowClobber", $true) + } + + Install-Module @InstallArgs + Write-Verbose -Message "Successfully installed AzureAD" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to install AzureAD module. Error message: $($_.Exception.Message)"; break + } + } + + try { + # Get installed Azure AD module + $AzureADModules = Get-Module -Name "AzureAD" -ListAvailable -ErrorAction Stop -Verbose:$false + + if ($AzureADModules -ne $null) { + # Check if multiple modules exist and determine the module path for the most current version + if (($AzureADModules | Measure-Object).Count -gt 1) { + $LatestAzureADModule = ($AzureADModules | Select-Object -Property Version | Sort-Object)[-1] + $AzureADModulePath = $AzureADModules | Where-Object { $_.Version -like $LatestAzureADModule.Version } | Select-Object -ExpandProperty ModuleBase + } + else { + $AzureADModulePath = $AzureADModules | Select-Object -ExpandProperty ModuleBase + } + + try { + # Construct array for required assemblies from Azure AD module + $Assemblies = @( + (Join-Path -Path $AzureADModulePath -ChildPath "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"), + (Join-Path -Path $AzureADModulePath -ChildPath "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll") + ) + + # Load required assemblies + Add-Type -Path $Assemblies -ErrorAction Stop -Verbose:$false + + try { + # Construct variable for authority URI + switch ($PSCmdlet.ParameterSetName) { + "AuthAppOnly" { + $Authority = "https://login.microsoftonline.com/$($TenantName)" + } + default { + $Authority = "https://login.microsoftonline.com/$($TenantName)/oauth2/v2.0/token" + } + } + + # Construct new authentication context + $AuthenticationContext = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $Authority -ErrorAction Stop + + # Construct platform parameters + $PlatformParams = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList $PromptBehavior -ErrorAction Stop + + try { + # Determine parameters when acquiring token + Write-Verbose -Message "Currently running in parameter set context: $($PSCmdlet.ParameterSetName)" + switch ($PSCmdlet.ParameterSetName) { + "AuthPrompt" { + # Acquire access token + Write-Verbose -Message "Attempting to acquire access token using user delegation" + $AuthenticationResult = ($AuthenticationContext.AcquireTokenAsync($Resource, $ClientID, $RedirectUri, $PlatformParams)).Result + } + "AuthCredential" { + # Construct required identity model user password credential + Write-Verbose -Message "Attempting to acquire access token using legacy user delegation with username and password" + $UserPasswordCredential = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.UserPasswordCredential" -ArgumentList ($Credential.UserName, $Credential.Password) -ErrorAction Stop + + # Acquire access token + $AuthenticationResult = ([Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContextIntegratedAuthExtensions]::AcquireTokenAsync($AuthenticationContext, $Resource, $ClientID, $UserPasswordCredential)).Result + } + "AuthAppOnly" { + # Construct required identity model client credential + Write-Verbose -Message "Attempting to acquire access token using app-based authentication" + $ClientCredential = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential" -ArgumentList ($ClientID, $ClientSecret) -ErrorAction Stop + + # Acquire access token + $AuthenticationResult = ($AuthenticationContext.AcquireTokenAsync($Resource, $ClientCredential)).Result + } + } + + # Check if access token was acquired + if ($AuthenticationResult.AccessToken -ne $null) { + Write-Verbose -Message "Successfully acquired an access token for authentication" + + # Construct authentication hash table for holding access token and header information + $Authentication = @{ + "Content-Type" = "application/json" + "Authorization" = -join("Bearer ", $AuthenticationResult.AccessToken) + "ExpiresOn" = $AuthenticationResult.ExpiresOn + } + + # Return the authentication token + return $Authentication + } + else { + Write-Warning -Message "Failure to acquire access token. Response with access token was null"; break + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred when attempting to call AcquireTokenAsync method. Error message: $($_.Exception.Message)"; break + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred when constructing an authentication token. Error message: $($_.Exception.Message)"; break + } + } + catch [System.Exception] { + Write-Warning -Message "Unable to load required assemblies from AzureAD module to construct an authentication token. Error message: $($_.Exception.Message)"; break + } + } + else { + Write-Warning -Message "Azure AD PowerShell module is not present on this system, please install before you continue"; break + } + } + catch [System.Exception] { + Write-Warning -Message "Unable to load required AzureAD module to for retrieving an authentication token. Error message: $($_.Exception.Message)"; break + } + } +} + +function Set-MSIntuneAdminConsent { + <# + .SYNOPSIS + Grant admin consent for delegated admin permissions. + NOTE: This function requires that AzureAD module is installed. Use 'Install-Module -Name AzureAD' to install it. + + .PARAMETER TenantName + A tenant name should be provided in the following format: tenantname.onmicrosoft.com. + + .PARAMETER ClientID + Specify a Global Admin user principal name. + + .EXAMPLE + # Grant admin consent for delegated admin permissions for an Intune tenant: + Set-MSIntuneAdminConsent -TenantName domain.onmicrsoft.com -UserPrincipalName "globaladmin@domain.onmicrosoft.com" .NOTES Author: Nickolaj Andersen Contact: @NickolajA - Created: 2017-09-27 - Updated: 2017-09-27 + Created: 2018-01-28 + Updated: 2018-01-28 Version history: - 1.0.0 - (2017-09-27) Script created - 1.0.1 - (2017-09-28) N/A - module manifest update - 1.0.2 - (2017-10-08) Added ExpiresOn property + 1.0.0 - (2018-01-28) Function created + 1.0.1 - (2018-01-28) Added static prompt behavior parameter with value of Auto #> [CmdletBinding()] @@ -34,27 +299,23 @@ function Get-MSIntuneAuthToken { [ValidateNotNullOrEmpty()] [string]$TenantName, - [parameter(Mandatory=$true, HelpMessage="Application ID for an Azure AD application.")] - [ValidateNotNullOrEmpty()] - [string]$ClientID, - - [parameter(Mandatory=$false, HelpMessage="Redirect URI for Azure AD application. Leave empty to leverage Azure PowerShell well known redirect URI.")] + [parameter(Mandatory=$true, HelpMessage="Specify a Global Admin user principal name.")] [ValidateNotNullOrEmpty()] - [string]$RedirectUri = "urn:ietf:wg:oauth:2.0:oob" + [string]$UserPrincipalName ) try { # Get installed Azure AD modules - $AzureADModules = Get-InstalledModule -Name "AzureAD" -ErrorAction Stop -Verbose:$false + $AzureADModules = Get-Module -Name "AzureAD" -ListAvailable -ErrorAction Stop -Verbose:$false if ($AzureADModules -ne $null) { # Check if multiple modules exist and determine the module path for the most current version if (($AzureADModules | Measure-Object).Count -gt 1) { $LatestAzureADModule = ($AzureADModules | Select-Object -Property Version | Sort-Object)[-1] - $AzureADModulePath = $AzureADModules | Where-Object { $_.Version -like $LatestAzureADModule.Version } | Select-Object -ExpandProperty InstalledLocation + $AzureADModulePath = $AzureADModules | Where-Object { $_.Version -like $LatestAzureADModule.Version } | Select-Object -ExpandProperty ModuleBase } else { - $AzureADModulePath = Get-InstalledModule -Name "AzureAD" | Select-Object -ExpandProperty InstalledLocation + $AzureADModulePath = Get-Module -Name "AzureAD" -ListAvailable -ErrorAction Stop -Verbose:$false | Select-Object -ExpandProperty ModuleBase } # Construct array for required assemblies from Azure AD module @@ -65,18 +326,24 @@ function Get-MSIntuneAuthToken { Add-Type -Path $Assemblies -ErrorAction Stop try { + # Set static variables $Authority = "https://login.microsoftonline.com/$($TenantName)/oauth2/token" $ResourceRecipient = "https://graph.microsoft.com" + $RedirectUri = "urn:ietf:wg:oauth:2.0:oob" + $ClientID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547" # Default Microsoft Intune PowerShell enterprise application # Construct new authentication context - $AuthenticationContext = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $Authority + $AuthenticationContext = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $Authority -ErrorAction Stop # Construct platform parameters - $PlatformParams = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Always" # Arguments: Auto, Always, Never, RefreshSession + $PlatformParams = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto" -ErrorAction Stop - # Acquire access token - $AuthenticationResult = ($AuthenticationContext.AcquireTokenAsync($ResourceRecipient, $ClientID, $RedirectUri, $PlatformParams)).Result - + # Construct user identifier + $UserIdentifier = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($UserPrincipalName, "OptionalDisplayableId") + + # Acquire authentication token and invoke admin consent + $AuthenticationResult = ($AuthenticationContext.AcquireTokenAsync($ResourceRecipient, $ClientID, $RedirectUri, $PlatformParams, $UserIdentifier, "prompt=admin_consent")).Result + # Check if access token was acquired if ($AuthenticationResult.AccessToken -ne $null) { # Construct authentication hash table for holding access token and header information @@ -103,5 +370,5 @@ function Get-MSIntuneAuthToken { } catch [System.Exception] { Write-Warning -Message "Unable to load required assemblies (Azure AD PowerShell module) to construct an authentication token. Error: $($_.Exception.Message)" ; break - } + } } \ No newline at end of file diff --git a/Modules/README.md b/Modules/README.md new file mode 100644 index 0000000..607fbb4 --- /dev/null +++ b/Modules/README.md @@ -0,0 +1,4 @@ +# PSIntuneAuth +Provides a function to retrieve an authentication token for Intune Graph API calls. + +![PowerShell Gallery](https://img.shields.io/powershellgallery/dt/PSIntuneAuth) diff --git a/Montoring/CustomInventory/App Inventory.workbook b/Montoring/CustomInventory/App Inventory.workbook new file mode 100644 index 0000000..366aeee --- /dev/null +++ b/Montoring/CustomInventory/App Inventory.workbook @@ -0,0 +1,504 @@ +{ + "version": "Notebook/1.0", + "items": [ + { + "type": 1, + "content": { + "json": "# Application Inventory\r\n---\r\n" + }, + "name": "text - 11" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "69110cf3-b63e-4a2a-acff-b4153066e98f", + "version": "KqlParameterItem/1.0", + "name": "TimeRange", + "type": 4, + "isRequired": true, + "value": { + "durationMs": 172800000 + }, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 3600000 + }, + { + "durationMs": 14400000 + }, + { + "durationMs": 43200000 + }, + { + "durationMs": 86400000 + }, + { + "durationMs": 172800000 + }, + { + "durationMs": 259200000 + }, + { + "durationMs": 604800000 + }, + { + "durationMs": 1209600000 + }, + { + "durationMs": 2419200000 + }, + { + "durationMs": 2592000000 + }, + { + "durationMs": 5184000000 + }, + { + "durationMs": 7776000000 + } + ], + "allowCustom": true + } + }, + { + "id": "8a108a50-e90e-4f47-91cd-fdc3c3b4108f", + "version": "KqlParameterItem/1.0", + "name": "Applications", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "let AppInventory = AppInventory_CL;\r\nAppInventory\r\n| where AppName_s <> \"\"\r\n| distinct AppName_s\r\n| sort by AppName_s asc", + "value": [ + "value::all" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "timeContext": { + "durationMs": 0 + }, + "timeContextFromParameter": "TimeRange", + "defaultValue": "value::1", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "parameters - 0" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AppInventory_CL\r\n | where AppName_s in ({Applications}) or '*' in ({Applications})\r\n | summarize arg_max(TimeGenerated, *) by ManagedDeviceID_g, AppName_s \r\n | summarize TotalCount = count() by AppName_s \r\n | join kind=inner (AppInventory_CL\r\n | make-series Trend = count() default = 0 on bin(TimeGenerated, 1d) in range(ago(2d), now(), 1h) by AppName_s\r\n )\r\n on AppName_s\r\n | order by TotalCount desc, AppName_s asc\r\n | serialize Id = row_number()\r\n | project Id, AppName = AppName_s, TotalCount, Trend", + "size": 2, + "timeContext": { + "durationMs": 172800000 + }, + "timeContextFromParameter": "TimeRange", + "exportFieldName": "AppName", + "exportParameterName": "AppName_s", + "exportDefaultValue": "(none)", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Id", + "formatter": 5 + }, + { + "columnMatch": "Trend", + "formatter": 9, + "formatOptions": { + "palette": "redGreen" + } + }, + { + "columnMatch": "ParentId", + "formatter": 5 + }, + { + "columnMatch": "Application Count", + "formatter": 8, + "formatOptions": { + "palette": "blue" + } + } + ], + "hierarchySettings": { + "idColumn": "Id", + "parentColumn": "ParentId", + "treeType": 0, + "expanderColumn": "Name" + }, + "sortBy": [ + { + "itemKey": "AppName", + "sortOrder": 1 + } + ] + }, + "sortBy": [ + { + "itemKey": "AppName", + "sortOrder": 1 + } + ] + }, + "name": "query - 1" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AppInventory_CL\r\n| where AppName_s == \"{AppName_s}\" \r\n| summarize arg_max(TimeGenerated, *) by ManagedDeviceID_g, AppName_s\r\n| project ComputerName_s, AppName_s, AppVersion_s, TimeGenerated\r\n| summarize count () by AppVersion_s", + "size": 1, + "title": "{AppName_s} version counts", + "timeContext": { + "durationMs": 172800000 + }, + "timeContextFromParameter": "TimeRange", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "graphSettings": { + "type": 0, + "topContent": { + "columnMatch": "AppVersion_s", + "formatter": 1 + }, + "centerContent": { + "columnMatch": "count_", + "formatter": 1, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + } + }, + "conditionalVisibility": { + "parameterName": "AppName_s", + "comparison": "isNotEqualTo", + "value": "(none)" + }, + "customWidth": "50", + "name": "query - AppVersion counts" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AppInventory_CL\r\n| where AppName_s == \"{AppName_s}\" \r\n| summarize arg_max(TimeGenerated, *) by ManagedDeviceID_g, AppName_s\r\n| project TimeGenerated, ComputerName_s, AppName_s, AppVersion_s", + "size": 0, + "showAnalytics": true, + "title": "{AppName_s} installed device list", + "timeContext": { + "durationMs": 172800000 + }, + "timeContextFromParameter": "TimeRange", + "showExportToExcel": true, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "customWidth": "50", + "name": "query - 12" + }, + { + "type": 1, + "content": { + "json": "\r\n\r\n-------------------\r\n\r\n# Device Specific App Inventory\r\n\r\n-----------------" + }, + "name": "text - 10", + "styleSettings": { + "margin": "0px", + "padding": "0px" + } + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "ad1dcea9-03e0-4fcb-a27f-0f7af0a17da2", + "version": "KqlParameterItem/1.0", + "name": "TimeRange2", + "label": "TimeRange", + "type": 4, + "isRequired": true, + "value": { + "durationMs": 1209600000 + }, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 3600000 + }, + { + "durationMs": 14400000 + }, + { + "durationMs": 43200000 + }, + { + "durationMs": 86400000 + }, + { + "durationMs": 172800000 + }, + { + "durationMs": 259200000 + }, + { + "durationMs": 604800000 + }, + { + "durationMs": 1209600000 + }, + { + "durationMs": 2419200000 + }, + { + "durationMs": 2592000000 + }, + { + "durationMs": 5184000000 + }, + { + "durationMs": 7776000000 + } + ] + } + }, + { + "id": "ed3d2d0d-1465-4d2a-9020-4384800d1bc1", + "version": "KqlParameterItem/1.0", + "name": "Device", + "type": 2, + "isRequired": true, + "query": "DeviceInventory_CL\r\n| summarize arg_max(TimeGenerated, *) by ManagedDeviceID_g\r\n| project ComputerName_s\r\n", + "value": "MVP24-007", + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 7776000000 + }, + "timeContextFromParameter": "TimeRange2", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "parameters - 6", + "styleSettings": { + "margin": "0px" + } + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let DeviceInventory = DeviceInventory_CL;\r\nlet AppInventory = AppInventory_CL;\r\nAppInventory \r\n| join kind = inner DeviceInventory on ManagedDeviceID_g\r\n| where ComputerName_s == \"{Device}\"\r\n| summarize arg_max(TimeGenerated, *) by AppName_s\r\n| summarize AppCount = count (AppName_s)\r\n| extend TitleText = \"Application Installed\"\r\n", + "size": 1, + "timeContext": { + "durationMs": 1209600000 + }, + "timeContextFromParameter": "TimeRange2", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "TitleText", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "AppCount", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal", + "maximumFractionDigits": 2, + "maximumSignificantDigits": 3 + } + } + }, + "showBorder": false + } + }, + "customWidth": "20", + "name": "query - 9" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let DeviceInventory = DeviceInventory_CL;\r\nlet AppInventory = AppInventory_CL;\r\nAppInventory \r\n| join kind = inner DeviceInventory on ManagedDeviceID_g\r\n| where ComputerName_s == \"{Device}\"\r\n| summarize arg_max(TimeGenerated, *) by AppName_s\r\n| project ComputerName_s, AppName_s, AppVersion_s, AppUninstallString_s\r\n| sort by AppName_s asc \r\n", + "size": 1, + "timeContext": { + "durationMs": 1209600000 + }, + "timeContextFromParameter": "TimeRange2", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "gridSettings": { + "formatters": [ + { + "columnMatch": "AppName_s", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "60ch" + } + }, + { + "columnMatch": "max_TimeGenerated_AppVersion_s", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "22.7143ch" + } + }, + { + "columnMatch": "max_TimeGenerated_AppUninstallString_s", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "117.4286ch" + } + } + ], + "labelSettings": [ + { + "columnId": "AppName_s", + "label": "Application" + } + ] + } + }, + "customWidth": "80", + "name": "query - 5" + }, + { + "type": 1, + "content": { + "json": "----\r\n# M365 Apps for Enterprise\r\n----" + }, + "name": "text - 8" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "f39d688a-9617-42f7-9db1-317f44f1f3fc", + "version": "KqlParameterItem/1.0", + "name": "M365TimeRange", + "type": 4, + "isRequired": true, + "value": { + "durationMs": 1209600000 + }, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 259200000 + }, + { + "durationMs": 604800000 + }, + { + "durationMs": 1209600000 + }, + { + "durationMs": 2419200000 + }, + { + "durationMs": 2592000000 + }, + { + "durationMs": 5184000000 + }, + { + "durationMs": 7776000000 + } + ], + "allowCustom": true + }, + "timeContext": { + "durationMs": 86400000 + } + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "parameters - 14" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let AppInventory = AppInventory_CL;\r\nAppInventory \r\n| where AppName_s contains \"Microsoft 365 Apps for enterprise\"\r\n| summarize arg_max(TimeGenerated, *) by ManagedDeviceID_g\r\n| summarize count () by AppVersion_s", + "size": 1, + "timeContext": { + "durationMs": 0 + }, + "timeContextFromParameter": "M365TimeRange", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart" + }, + "name": "query - 7" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let startTime = {M365TimeRange:start};\r\nlet endTime = {M365TimeRange:end};\r\nAppInventory_CL\r\n| where AppName_s contains \"Microsoft 365 Apps for enterprise\"\r\n| summarize arg_max(TimeGenerated, *) by AppVersion_s\r\n| make-series num=dcount(ManagedDeviceID_g) default=0 on TimeGenerated in range(startTime, endTime, 1d) by AppVersion_s\r\n| render timechart ", + "size": 1, + "aggregation": 5, + "timeContext": { + "durationMs": 0 + }, + "timeContextFromParameter": "M365TimeRange", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "areachart", + "chartSettings": { + "showLegend": true + } + }, + "name": "query - 15" + } + ], + "fallbackResourceIds": [ + "/subscriptions/227b387f-e74b-4160-aa2e-a0d34eba2168/resourcegroups/rg_logs/providers/microsoft.operationalinsights/workspaces/mvp-loganalytics" + ], + "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" +} \ No newline at end of file diff --git a/Montoring/CustomInventory/Device Inventory.workbook b/Montoring/CustomInventory/Device Inventory.workbook new file mode 100644 index 0000000..eb88197 --- /dev/null +++ b/Montoring/CustomInventory/Device Inventory.workbook @@ -0,0 +1,274 @@ +{ + "version": "Notebook/1.0", + "items": [ + { + "type": 1, + "content": { + "json": "# Device Inventory\r\n---\r\n" + }, + "name": "text - 11" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "ad1dcea9-03e0-4fcb-a27f-0f7af0a17da2", + "version": "KqlParameterItem/1.0", + "name": "TimeRange2", + "label": "TimeRange", + "type": 4, + "isRequired": true, + "value": { + "durationMs": 2592000000 + }, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 3600000 + }, + { + "durationMs": 14400000 + }, + { + "durationMs": 43200000 + }, + { + "durationMs": 86400000 + }, + { + "durationMs": 172800000 + }, + { + "durationMs": 259200000 + }, + { + "durationMs": 604800000 + }, + { + "durationMs": 1209600000 + }, + { + "durationMs": 2419200000 + }, + { + "durationMs": 2592000000 + }, + { + "durationMs": 5184000000 + }, + { + "durationMs": 7776000000 + } + ] + } + }, + { + "id": "60003c98-2170-459c-9158-26ae9af286bf", + "version": "KqlParameterItem/1.0", + "name": "DeviceName", + "type": 1, + "description": "Default \"All Devices\"", + "value": "", + "timeContext": { + "durationMs": 0 + }, + "timeContextFromParameter": "TimeRange2" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "parameters - 6", + "styleSettings": { + "margin": "0px" + } + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "DeviceInventory_CL\r\n| where \"{DeviceName:escape}\" =~ \"*\" or ComputerName_s contains \"{DeviceName:escape}\" or \"{DeviceName:escape}\" =~ \"All devices\"\r\n| where isnotempty(SerialNumber_s) \r\n| extend BitlockerCipher_s = iff(isempty(BitlockerCipher_s), \"NoBitlocker\", BitlockerCipher_s)\r\n| project TimeGenerated, SerialNumber_s, BitlockerCipher_s, ComputerName_s\r\n| summarize arg_max (TimeGenerated, *) by SerialNumber_s\r\n//| evaluate pivot (BitlockerCipher_s)\r\n| summarize dcount(SerialNumber_s) by BitlockerCipher_s", + "size": 1, + "title": "Bitlocker Status", + "timeContext": { + "durationMs": 0 + }, + "timeContextFromParameter": "TimeRange2", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "tileSettings": { + "showBorder": false + }, + "chartSettings": { + "seriesLabelSettings": [ + { + "seriesName": "XtsAes128", + "color": "blue" + }, + { + "seriesName": "NoBitlocker", + "color": "redBright" + }, + { + "seriesName": "XtsAes256", + "color": "green" + } + ] + } + }, + "customWidth": "50", + "name": "query - 4" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "DeviceInventory_CL\r\n| where isnotempty(Model_s)\r\n| where \"{DeviceName:escape}\" =~ \"*\" or ComputerName_s contains \"{DeviceName:escape}\" or \"{DeviceName:escape}\" =~ \"All devices\"\r\n| summarize arg_max (TimeGenerated, *) by SerialNumber_s\r\n| summarize dcount(SerialNumber_s) by DefaultAUService_s", + "size": 1, + "title": "Windows Update Service", + "timeContext": { + "durationMs": 0 + }, + "timeContextFromParameter": "TimeRange2", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "piechart", + "tileSettings": { + "showBorder": false + }, + "chartSettings": { + "seriesLabelSettings": [ + { + "seriesName": "XtsAes128", + "color": "blue" + }, + { + "seriesName": "NoBitlocker", + "color": "redBright" + }, + { + "seriesName": "XtsAes256", + "color": "green" + } + ] + } + }, + "customWidth": "50", + "name": "query - 4 - Copy" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "IntuneDevices\r\n| where isnotempty(StorageTotal) and StorageTotal != 0 and OS == \"Windows\"\r\n| where \"{DeviceName:escape}\" =~ \"*\" or DeviceName contains \"{DeviceName:escape}\" or \"{DeviceName:escape}\" =~ \"All devices\"\r\n| summarize arg_max (LastContact, *) by SerialNumber\r\n| extend StorageFreeRate = (StorageFree * 100 / StorageTotal) \r\n| extend StorageTotal = strcat (StorageTotal / 1024, \" GB\")\r\n| extend StorageFree = strcat (StorageFree / 1024, \" GB\") \r\n| project TimeGenerated, DeviceName, StorageTotal, StorageFree, StorageFreeRate", + "size": 0, + "showAnalytics": true, + "title": "Storage Status", + "timeContext": { + "durationMs": 0 + }, + "timeContextFromParameter": "TimeRange2", + "showExportToExcel": true, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "StorageFreeRate", + "formatter": 8, + "formatOptions": { + "min": 20, + "max": 50, + "palette": "redGreen" + } + } + ], + "sortBy": [ + { + "itemKey": "DeviceName", + "sortOrder": 1 + } + ] + }, + "sortBy": [ + { + "itemKey": "DeviceName", + "sortOrder": 1 + } + ] + }, + "name": "query - 9" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let data_DeviceInventory = DeviceInventory_CL\r\n | where \"{DeviceName:escape}\" =~ \"*\" or ComputerName_s contains \"{DeviceName:escape}\" or \"{DeviceName:escape}\" =~ \"All devices\";\r\nlet data_IntuneDevice = IntuneDevices\r\n | where \"{DeviceName:escape}\" =~ \"*\" or DeviceName contains \"{DeviceName:escape}\" or \"{DeviceName:escape}\" =~ \"All devices\";\r\ndata_DeviceInventory\r\n| join kind=innerunique IntuneDevices on $left.ManagedDeviceID_g == $right.DeviceId\r\n| summarize arg_max (LastContact, *) by SerialNumber\r\n| project TimeGenerated, ComputerName_s, UPN, UserName, CompliantState, Ownership, ManagedBy, JoinType, ManagedDeviceID_g, AADDeviceID = ReferenceId, Model_s, Manufacturer_s, ComputerUpTime_s, LastBoot_s, InstallDate_s, WindowsVersion_s, SkuFamily, DefaultAUService_s, SerialNumber_s, BiosVersion_s, BiosDate_s, FirmwareType_s, Memory_s, OSBuild_s, OSRevision_s, CPUManufacturer_s, CPUName_s, CPUCores_s, CPULogical_s, TPMReady_s, TPMPresent_s, TPMEnabled_s, TPMActived_s, BitlockerCipher_s, BitlockerVolumeStatus_s, BitlockerProtectionStatus_s, NetInterfaceDescription_s, NetProfileName_s, NetIPv4Adress_s, NetInterfaceAlias_s, NetIPv4DefaultGateway_s, StorageTotal, StorageFree", + "size": 0, + "showAnalytics": true, + "title": "Device Inventory Summarize", + "timeContext": { + "durationMs": 0 + }, + "timeContextFromParameter": "TimeRange2", + "showExportToExcel": true, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "gridSettings": { + "sortBy": [ + { + "itemKey": "ComputerName_s", + "sortOrder": 1 + } + ] + }, + "sortBy": [ + { + "itemKey": "ComputerName_s", + "sortOrder": 1 + } + ] + }, + "name": "query - 3" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "DeviceInventory_CL\r\n| where \"{DeviceName:escape}\" =~ \"*\" or ComputerName_s contains \"{DeviceName:escape}\" or \"{DeviceName:escape}\" =~ \"All devices\"\r\n| where isnotempty(ComputerCountry_s) \r\n| summarize arg_max (TimeGenerated, * ) by SerialNumber_s\r\n| project ComputerCountry_s, ComputerCity_s\r\n| summarize count() by ComputerCountry_s, ComputerCity_s", + "size": 0, + "timeContext": { + "durationMs": 2592000000 + }, + "timeContextFromParameter": "TimeRange2", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "visualization": "map", + "mapSettings": { + "locInfo": "CountryRegion", + "locInfoColumn": "ComputerCountry_s", + "sizeSettings": "count_", + "sizeAggregation": "Count", + "legendMetric": "count_", + "legendAggregation": "Sum", + "itemColorSettings": { + "nodeColorField": "count_", + "colorAggregation": "Sum", + "type": "heatmap", + "heatmapPalette": "greenRed" + } + } + }, + "name": "query - 6" + } + ], + "fallbackResourceIds": [ + "/subscriptions/227b387f-e74b-4160-aa2e-a0d34eba2168/resourcegroups/rg_logs/providers/microsoft.operationalinsights/workspaces/mvp-loganalytics" + ], + "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" +} \ No newline at end of file diff --git a/Montoring/CustomInventory/Invoke-CustomInventory.ps1 b/Montoring/CustomInventory/Invoke-CustomInventory.ps1 new file mode 100644 index 0000000..2f070ee --- /dev/null +++ b/Montoring/CustomInventory/Invoke-CustomInventory.ps1 @@ -0,0 +1,360 @@ +<# +.SYNOPSIS + Collect custom device inventory and upload to Log Analytics for further processing. + +.DESCRIPTION + This script will collect device hardware and / or app inventory and upload this to a Log Analytics Workspace. This allows you to easily search in device hardware and installed apps inventory. + The script is meant to be runned on a daily schedule either via Proactive Remediations (RECOMMENDED) in Intune or manually added as local schedule task on your Windows 10 Computer. + +.EXAMPLE + Invoke-CustomInventory.ps1 (Required to run as System or Administrator) + +.NOTES + FileName: Invoke-CustomInventory.ps1 + Author: Jan Ketil Skanke + Contributor: Sandy Zeng + Contact: @JankeSkanke + Created: 2021-01-02 + Updated: 2021-04-05 + + Version history: + 0.9.0 - (2021-01-02) Script created + 1.0.0 - (2021-01-02) Script polished cleaned up. + 1.0.1 - (2021-04-05) Added NetworkAdapter array and fixed typo +#> +#region initialize +# Enable TLS 1.2 support +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +# Replace with your Log Analytics Workspace ID +$CustomerId = "" + +# Replace with your Primary Key +$SharedKey = "" + +#Control if you want to collect App or Device Inventory or both (True = Collect) +$CollectAppInventory = $true +$CollectDeviceInventory = $true + +# You can use an optional field to specify the timestamp from the data. If the time field is not specified, Azure Monitor assumes the time is the message ingestion time +# DO NOT DELETE THIS VARIABLE. Recommened keep this blank. +$TimeStampField = "" + +#endregion initialize + +#region functions + +# Function to get all Installed Application +function Get-InstalledApplications() { + param( + [string]$UserSid + ) + + New-PSDrive -PSProvider Registry -Name "HKU" -Root HKEY_USERS | Out-Null + $regpath = @("HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*") + $regpath += "HKU:\$UserSid\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" + if (-not ([IntPtr]::Size -eq 4)) { + $regpath += "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + $regpath += "HKU:\$UserSid\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + } + $propertyNames = 'DisplayName', 'DisplayVersion', 'Publisher', 'UninstallString' + $Apps = Get-ItemProperty $regpath -Name $propertyNames -ErrorAction SilentlyContinue | . { process { if ($_.DisplayName) { $_ } } } | Select-Object DisplayName, DisplayVersion, Publisher, UninstallString, PSPath | Sort-Object DisplayName + Remove-PSDrive -Name "HKU" | Out-Null + Return $Apps +} + +# Function to create the authorization signature +Function New-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) { + $xHeaders = "x-ms-date:" + $date + $stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource + + $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash) + $keyBytes = [Convert]::FromBase64String($sharedKey) + + $sha256 = New-Object System.Security.Cryptography.HMACSHA256 + $sha256.Key = $keyBytes + $calculatedHash = $sha256.ComputeHash($bytesToHash) + $encodedHash = [Convert]::ToBase64String($calculatedHash) + $authorization = 'SharedKey {0}:{1}' -f $customerId, $encodedHash + return $authorization +} + +# Function to create and post the request +Function Send-LogAnalyticsData($customerId, $sharedKey, $body, $logType) { + $method = "POST" + $contentType = "application/json" + $resource = "/api/logs" + $rfc1123date = [DateTime]::UtcNow.ToString("r") + $contentLength = $body.Length + $signature = New-Signature ` + -customerId $customerId ` + -sharedKey $sharedKey ` + -date $rfc1123date ` + -contentLength $contentLength ` + -method $method ` + -contentType $contentType ` + -resource $resource + $uri = "https://" + $customerId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01" + + #validate that payload data does not exceed limits + if ($body.Length -gt (31.9 *1024*1024)) + { + throw("Upload payload is too big and exceed the 32Mb limit for a single upload. Please reduce the payload size. Current payload size is: " + ($body.Length/1024/1024).ToString("#.#") + "Mb") + } + + $payloadsize = ("Upload payload size is " + ($body.Length/1024).ToString("#.#") + "Kb ") + + $headers = @{ + "Authorization" = $signature; + "Log-Type" = $logType; + "x-ms-date" = $rfc1123date; + "time-generated-field" = $TimeStampField; + } + + $response = Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Headers $headers -Body $body -UseBasicParsing + $statusmessage = "$($response.StatusCode) : $($payloadsize)" + return $statusmessage +} +function Start-PowerShellSysNative { + param ( + [parameter(Mandatory = $false, HelpMessage = "Specify arguments that will be passed to the sysnative PowerShell process.")] + [ValidateNotNull()] + [string]$Arguments + ) + + # Get the sysnative path for powershell.exe + $SysNativePowerShell = Join-Path -Path ($PSHOME.ToLower().Replace("syswow64", "sysnative")) -ChildPath "powershell.exe" + + # Construct new ProcessStartInfo object to run scriptblock in fresh process + $ProcessStartInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo + $ProcessStartInfo.FileName = $SysNativePowerShell + $ProcessStartInfo.Arguments = $Arguments + $ProcessStartInfo.RedirectStandardOutput = $true + $ProcessStartInfo.RedirectStandardError = $true + $ProcessStartInfo.UseShellExecute = $false + $ProcessStartInfo.WindowStyle = "Hidden" + $ProcessStartInfo.CreateNoWindow = $true + + # Instatiate the new 64-bit process + $Process = [System.Diagnostics.Process]::Start($ProcessStartInfo) + + # Read standard error output to determine if the 64-bit script process somehow failed + $ErrorOutput = $Process.StandardError.ReadToEnd() + if ($ErrorOutput) { + Write-Error -Message $ErrorOutput + } +}#endfunction +#endregion functions + +#region script +#Get Common data for App and Device Inventory: +#Get Intune DeviceID and ManagedDeviceName +if (@(Get-ChildItem HKLM:SOFTWARE\Microsoft\Enrollments\ -Recurse | Where-Object { $_.PSChildName -eq 'MS DM Server' })) { + $MSDMServerInfo = Get-ChildItem HKLM:SOFTWARE\Microsoft\Enrollments\ -Recurse | Where-Object { $_.PSChildName -eq 'MS DM Server' } + $ManagedDeviceInfo = Get-ItemProperty -LiteralPath "Registry::$($MSDMServerInfo)" +} +$ManagedDeviceName = $ManagedDeviceInfo.EntDeviceName +$ManagedDeviceID = $ManagedDeviceInfo.EntDMID +#Get Computer Info +$ComputerInfo = Get-ComputerInfo +$ComputerName = $ComputerInfo.CsName +$ComputerManufacturer = $ComputerInfo.CsManufacturer + +#region DEVICEINVENTORY +if ($CollectDeviceInventory) { + #Set Name of Log + $DeviceLog = "DeviceInventory" + + #Get Intune DeviceID and ManagedDeviceName + if (@(Get-ChildItem HKLM:SOFTWARE\Microsoft\Enrollments\ -Recurse | Where-Object { $_.PSChildName -eq 'MS DM Server' })) { + $MSDMServerInfo = Get-ChildItem HKLM:SOFTWARE\Microsoft\Enrollments\ -Recurse | Where-Object { $_.PSChildName -eq 'MS DM Server' } + $ManagedDeviceInfo = Get-ItemProperty -LiteralPath "Registry::$($MSDMServerInfo)" + } + $ManagedDeviceName = $ManagedDeviceInfo.EntDeviceName + $ManagedDeviceID = $ManagedDeviceInfo.EntDMID + + #Get Windows Update Service Settings + $DefaultAUService = (New-Object -ComObject "Microsoft.Update.ServiceManager").Services | Where-Object { $_.isDefaultAUService -eq $True } | Select-Object Name + $AUMeteredNetwork = (Get-ItemProperty -Path HKLM:\Software\Microsoft\WindowsUpdate\UX\Settings\).AllowAutoWindowsUpdateDownloadOverMeteredNetwork + if ($AUMeteredNetwork -eq "0") { + $AUMetered = "false" + } + else { $AUMetered = "true" } + + #Get Device Location + $ComputerPublicIP = (Invoke-WebRequest -UseBasicParsing -Uri "http://ifconfig.me/ip").Content + $Computerlocation = Invoke-RestMethod -Method Get -Uri "http://ip-api.com/json/$ComputerPublicIP" + $ComputerCountry = $Computerlocation.country + $ComputerCity = $Computerlocation.city + + # Get Computer Inventory Information + $ComputerModel = $ComputerInfo.CsModel + $ComputerUptime = [int]($ComputerInfo.OsUptime).Days + $ComputerLastBoot = $ComputerInfo.OsLastBootUpTime + $ComputerInstallDate = $ComputerInfo.OsInstallDate + $ComputerWindowsVersion = $ComputerInfo.WindowsVersion + $ComputerSystemSkuNumber = $ComputerInfo.CsSystemSKUNumber + $ComputerSerialNr = $ComputerInfo.BiosSeralNumber + $ComputerBiosUUID = Get-WmiObject Win32_ComputerSystemProduct | Select-Object -ExpandProperty UUID + $ComputerBiosVersion = $ComputerInfo.BiosSMBIOSBIOSVersion + $ComputerBiosDate = $ComputerInfo.BiosReleaseDate + $ComputerFirmwareType = $ComputerInfo.BiosFirmwareType + $ComputerPCSystemType = $ComputerInfo.CsPCSystemType + $ComputerPCSystemTypeEx = $ComputerInfo.CsPCSystemTypeEx + $ComputerPhysicalMemory = [Math]::Round(($ComputerInfo.CsTotalPhysicalMemory / 1GB)) + $ComputerOSBuild = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name CurrentBuild).CurrentBuild + $ComputerOSRevision = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name UBR).UBR + $ComputerCPU = Get-CimInstance win32_processor | Select-Object Name, Manufacturer, NumberOfCores, NumberOfLogicalProcessors + $ComputerProcessorManufacturer = $ComputerCPU.Manufacturer | Get-Unique + $ComputerProcessorName = $ComputerCPU.Name | Get-Unique + $ComputerNumberOfCores = $ComputerCPU.NumberOfCores | Get-Unique + $ComputerNumberOfLogicalProcessors = $ComputerCPU.NumberOfLogicalProcessors | Get-Unique + $TPMValues = Get-Tpm -ErrorAction SilentlyContinue | Select-Object -Property TPMReady, TPMPresent, TPMEnabled, TPMActivated, ManagedAuthLevel + $BitLockerInfo = Get-BitLockerVolume -MountPoint C: | Select-Object -Property * + $ComputerTPMReady = $TPMValues.TPMReady + $ComputerTPMPresent = $TPMValues.TPMPresent + $ComputerTPMEnabled = $TPMValues.TPMEnabled + $ComputerTPMActivated = $TPMValues.TPMActivated + $ComputerTPMThumbprint = (Get-TpmEndorsementKeyInfo).AdditionalCertificates.Thumbprint + $ComputerBitlockerCipher = $BitLockerInfo.EncryptionMethod + $ComputerBitlockerStatus = $BitLockerInfo.VolumeStatus + $ComputerBitlockerProtection = $BitLockerInfo.ProtectionStatus + $ComputerDefaultAUService = $DefaultAUService.Name + $ComputerAUMetered = $AUMetered + + #$timestamp = Get-Date -Format "yyyy-MM-DDThh:mm:ssZ" + + #Get network adapters + $NetWorkArray = @() + + $CurrentNetAdapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } + + foreach ($CurrentNetAdapter in $CurrentNetAdapters) { + $IPConfiguration = Get-NetIPConfiguration -InterfaceIndex $CurrentNetAdapter[0].ifIndex + $ComputerNetInterfaceDescription = $CurrentNetAdapter.InterfaceDescription + $ComputerNetProfileName = $IPConfiguration.NetProfile.Name + $ComputerNetIPv4Adress = $IPConfiguration.IPv4Address.IPAddress + $ComputerNetInterfaceAlias = $CurrentNetAdapter.InterfaceAlias + $ComputerNetIPv4DefaultGateway = $IPConfiguration.IPv4DefaultGateway.NextHop + + $tempnetwork = New-Object -TypeName PSObject + $tempnetwork | Add-Member -MemberType NoteProperty -Name "NetInterfaceDescription" -Value "$ComputerNetInterfaceDescription" -Force + $tempnetwork | Add-Member -MemberType NoteProperty -Name "NetProfileName" -Value "$ComputerNetProfileName" -Force + $tempnetwork | Add-Member -MemberType NoteProperty -Name "NetIPv4Adress" -Value "$ComputerNetIPv4Adress" -Force + $tempnetwork | Add-Member -MemberType NoteProperty -Name "NetInterfaceAlias" -Value "$ComputerNetInterfaceAlias" -Force + $tempnetwork | Add-Member -MemberType NoteProperty -Name "NetIPv4DefaultGateway" -Value "$ComputerNetIPv4DefaultGateway" -Force + $NetWorkArray += $tempnetwork + } + [System.Collections.ArrayList]$NetWorkArrayList = $NetWorkArray + + # Create JSON to Upload to Log Analytics + $Inventory = New-Object System.Object + $Inventory | Add-Member -MemberType NoteProperty -Name "ManagedDeviceName" -Value "$ManagedDeviceName" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "ManagedDeviceID" -Value "$ManagedDeviceID" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "ComputerName" -Value "$ComputerName" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "Model" -Value "$ComputerModel" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "Manufacturer" -Value "$ComputerManufacturer" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "PCSystemType" -Value "$ComputerPCSystemType" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "PCSystemTypeEx" -Value "$ComputerPCSystemTypeEx" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "ComputerUpTime" -Value "$ComputerUptime" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "LastBoot" -Value "$ComputerLastBoot" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "InstallDate" -Value "$ComputerInstallDate" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "WindowsVersion" -Value "$ComputerWindowsVersion" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "DefaultAUService" -Value "$ComputerDefaultAUService" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "AUMetered" -Value "$ComputerAUMetered" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "SystemSkuNumber" -Value "$ComputerSystemSkuNumber" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "SerialNumber" -Value "$ComputerSerialNr" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "SMBIOSUUID" -Value "$ComputerBiosUUID" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "BiosVersion" -Value "$ComputerBiosVersion" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "BiosDate" -Value "$ComputerBiosDate" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "FirmwareType" -Value "$ComputerFirmwareType" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "Memory" -Value "$ComputerPhysicalMemory" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "OSBuild" -Value "$ComputerOSBuild" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "OSRevision" -Value "$ComputerOSRevision" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "CPUManufacturer" -Value "$ComputerProcessorManufacturer" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "CPUName" -Value "$ComputerProcessorName" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "CPUCores" -Value "$ComputerNumberOfCores" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "CPULogical" -Value "$ComputerNumberOfLogicalProcessors" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "TPMReady" -Value "$ComputerTPMReady" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "TPMPresent" -Value "$ComputerTPMPresent" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "TPMEnabled" -Value "$ComputerTPMEnabled" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "TPMActived" -Value "$ComputerTPMActivated" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "TPMThumbprint" -Value "$ComputerTPMThumbprint" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "BitlockerCipher" -Value "$ComputerBitlockerCipher" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "BitlockerVolumeStatus" -Value "$ComputerBitlockerStatus" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "BitlockerProtectionStatus" -Value "$ComputerBitlockerProtection" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "ComputerCountry" -Value "$ComputerCountry" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "ComputerCity" -Value "$ComputerCity" -Force + $Inventory | Add-Member -MemberType NoteProperty -Name "NetworkAdapters" -Value $NetWorkArrayList -Force + + $Devicejson = $Inventory | ConvertTo-Json + + # Submit the data to the API endpoint + $ResponseDeviceInventory = Send-LogAnalyticsData -customerId $customerId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($Devicejson)) -logType $DeviceLog +}#endregion DEVICEINVENTORY + +#region APPINVENTORY +if ($CollectAppInventory) { + $AppLog = "AppInventory" + + #Get SID of current interactive users + $CurrentLoggedOnUser = (Get-WmiObject -Class win32_computersystem).UserName + $AdObj = New-Object System.Security.Principal.NTAccount($CurrentLoggedOnUser) + $strSID = $AdObj.Translate([System.Security.Principal.SecurityIdentifier]) + $UserSid = $strSID.Value + #Get Apps for system and current user + $MyApps = Get-InstalledApplications -UserSid $UserSid + $UniqueApps = ($MyApps | Group-Object Displayname | Where-Object { $_.Count -eq 1 } ).Group + $DuplicatedApps = ($MyApps | Group-Object Displayname | Where-Object { $_.Count -gt 1 } ).Group + $NewestDuplicateApp = ($DuplicatedApps | Group-Object DisplayName) | ForEach-Object { $_.Group | Sort-Object [version]DisplayVersion -Descending | Select-Object -First 1 } + $CleanAppList = $UniqueApps + $NewestDuplicateApp | Sort-Object DisplayName + + $AppArray = @() + foreach ($App in $CleanAppList) { + $tempapp = New-Object -TypeName PSObject + $tempapp | Add-Member -MemberType NoteProperty -Name "ComputerName" -Value "$ComputerName" -Force + $tempapp | Add-Member -MemberType NoteProperty -Name "ManagedDeviceName" -Value "$ManagedDeviceName" -Force + $tempapp | Add-Member -MemberType NoteProperty -Name "ManagedDeviceID" -Value "$ManagedDeviceID" -Force + $tempapp | Add-Member -MemberType NoteProperty -Name "AppName" -Value $App.DisplayName -Force + $tempapp | Add-Member -MemberType NoteProperty -Name "AppVersion" -Value $App.DisplayVersion -Force + $tempapp | Add-Member -MemberType NoteProperty -Name "AppInstallDate" -Value $App.InstallDate -Force -ErrorAction SilentlyContinue + $tempapp | Add-Member -MemberType NoteProperty -Name "AppPublisher" -Value $App.Publisher -Force + $tempapp | Add-Member -MemberType NoteProperty -Name "AppUninstallString" -Value $App.UninstallString -Force + $tempapp | Add-Member -MemberType NoteProperty -Name "AppUninstallRegPath" -Value $app.PSPath.Split("::")[-1] + $AppArray += $tempapp + } + + $Appjson = $AppArray | ConvertTo-Json + + # Submit the data to the API endpoint + $ResponseAppInventory = Send-LogAnalyticsData -customerId $customerId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($Appjson)) -logType $AppLog +} +#endregion APPINVENTORY + +#Report back status +$date = Get-Date -Format "dd-MM HH:mm" +$OutputMessage = "InventoryDate:$date " + +if ($CollectDeviceInventory) { + if ($ResponseDeviceInventory -match "200 :") { + + $OutputMessage = $OutPutMessage + "DeviceInventory:OK " + $ResponseDeviceInventory + } + else { + $OutputMessage = $OutPutMessage + "DeviceInventory:Fail " + } +} +if ($CollectAppInventory) { + if ($ResponseAppInventory -match "200 :") { + + $OutputMessage = $OutPutMessage + " AppInventory:OK " + $ResponseAppInventory + } + else { + $OutputMessage = $OutPutMessage + " AppInventory:Fail " + } +} +Write-Output $OutputMessage +Exit 0 + + + +#endregion script \ No newline at end of file diff --git a/Montoring/CustomInventory/Readme.md b/Montoring/CustomInventory/Readme.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Montoring/CustomInventory/Readme.md @@ -0,0 +1 @@ + diff --git a/Montoring/Readme.md b/Montoring/Readme.md new file mode 100644 index 0000000..048632b --- /dev/null +++ b/Montoring/Readme.md @@ -0,0 +1 @@ +#Home of monitoring scripts diff --git a/Reporting/Azurefunction/LogAnalyticsRestApi/readme.md b/Reporting/Azurefunction/LogAnalyticsRestApi/readme.md new file mode 100644 index 0000000..218de2e --- /dev/null +++ b/Reporting/Azurefunction/LogAnalyticsRestApi/readme.md @@ -0,0 +1,2 @@ +# Azure Function for Log Analytics data injection +## Keeping secrets out of code. diff --git a/Script-TemplateWithAuth.ps1 b/Script-TemplateWithAuth.ps1 deleted file mode 100644 index bde9f84..0000000 --- a/Script-TemplateWithAuth.ps1 +++ /dev/null @@ -1,86 +0,0 @@ -<# -.SYNOPSIS - - -.DESCRIPTION - - -.PARAMETER Param - Param description. - -.PARAMETER ShowProgress - Show a progressbar displaying the current operation. - -.EXAMPLE - - -.NOTES - FileName: