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/Autopilot/Upload-WindowsAutopilotDeviceInfo.ps1 b/Autopilot/Upload-WindowsAutopilotDeviceInfo.ps1 new file mode 100644 index 0000000..b203d83 --- /dev/null +++ b/Autopilot/Upload-WindowsAutopilotDeviceInfo.ps1 @@ -0,0 +1,234 @@ +<#PSScriptInfo +.VERSION 1.1.0 +.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: 2019-10-29 + + 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. +#> +[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' + 'orderIdentifier' = 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)" + } + + try { + # Call Graph API and post Autopilot devices sync command + Write-Verbose -Message "Attempting to perform a sync action in Autopilot" + $GraphResource = "deviceManagement/windowsAutopilotSettings/sync" + $GraphURI = "https://graph.microsoft.com/$($GraphVersion)/$($GraphResource)" + (Invoke-RestMethod -Uri $GraphURI -Headers $AuthToken -Method Post -ErrorAction Stop -Verbose:$false).Value + } + 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 $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..d456de1 --- /dev/null +++ b/Certificates/Update-SCEPCertificate.ps1 @@ -0,0 +1,157 @@ +<# +.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: 2019-12-21 + + Version history: + 1.0.0 - (2019-12-21) Script created +#> +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") } + 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' or 'LAPTOP' + $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' or 'LAPTOP' + $SCEPCertificateItem = Get-SCEPCertificate + if ($SCEPCertificateItem -ne $null) { + # Remove existing SCEP issues certificate with subject name matching either 'DESKTOP' or 'LAPTOP' + 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/Driver Automation/Invoke-MSIntuneDriverUpdate.ps1 b/Driver Automation/Invoke-MSIntuneDriverUpdate.ps1 new file mode 100644 index 0000000..aee7e88 --- /dev/null +++ b/Driver Automation/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 = "/s /e /f `"$($DriverExtractDest)`"" + 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 "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/Modules/IntuneWin32App/IntuneWin32App.psd1 b/Modules/IntuneWin32App/IntuneWin32App.psd1 new file mode 100644 index 0000000..ff56333 --- /dev/null +++ b/Modules/IntuneWin32App/IntuneWin32App.psd1 @@ -0,0 +1,131 @@ +# +# Module manifest for module 'IntuneWin32App' +# +# Generated by: Nickolaj Andersen @NickolajA +# +# Generated on: 2020-01-04 +# + +@{ +# Script module or binary module file associated with this manifest. +RootModule = 'IntuneWin32App.psm1' + +# Version number of this module. +ModuleVersion = '1.1.0' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '2554f0a2-8047-49a1-bf6e-0108dc9263dc' + +# Author of this module +Author = 'Nickolaj Andersen' + +# Company or vendor of this module +CompanyName = 'SCConfigMgr.com' + +# Copyright statement for this module +Copyright = '(c) 2020 Nickolaj Andersen. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'Provides a set of functions to package and add a Win32 app to Microsoft Endpoint Manager (Intune).' + +# Minimum version of the Windows PowerShell engine required by this module +PowerShellVersion = '5.0' + +# Name of the Windows PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the Windows PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# CLRVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +RequiredModules = @("AzureAD", "PSIntuneAuth") + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# 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-IntuneWin32App',` + 'New-IntuneWin32AppPackage',` + 'Add-IntuneWin32App',` + 'Add-IntuneWin32AppAssignment',` + 'New-IntuneWin32AppDetectionRule',` + 'Get-IntuneWin32AppMetaData',` + 'New-IntuneWin32AppReturnCode',` + 'New-IntuneWin32AppIcon',` + 'Expand-IntuneWin32AppPackage',` + 'Get-MSIMetaData',` + "New-IntuneWin32AppRequirementRule" +) + +# 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 = @() + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases 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 aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + PSData = @{ + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/SCConfigMgr/Intune/tree/master/Modules/IntuneWin32App' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/Modules/IntuneWin32App/IntuneWin32App.psm1 b/Modules/IntuneWin32App/IntuneWin32App.psm1 new file mode 100644 index 0000000..2b9189e --- /dev/null +++ b/Modules/IntuneWin32App/IntuneWin32App.psm1 @@ -0,0 +1,2536 @@ +function Get-AuthToken { + <# + .SYNOPSIS + Get an authorization token from Azure AD. + + .DESCRIPTION + Get an authorization token from Azure AD. + + .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. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + [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" + ) + # 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 "AllUsers" -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 module" + } + 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 + } +} + +function Get-ErrorResponseBody { + <# + .SYNOPSIS + Get error details from Graph invocation. + + .DESCRIPTION + Get error details from Graph invocation. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + 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 Get-MSIMetaData { + <# + .SYNOPSIS + Retrieve a specific MSI property value from MSI based installation file. + + .DESCRIPTION + Retrieve a specific MSI property value from MSI based installation file. + + .PARAMETER Path + Specify the full path to a MSI based installation file. + + .PARAMETER Property + Specify the MSI database property to retrieve it's value. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-27) Function created + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [parameter(Mandatory = $true, HelpMessage = "Specify the full path to a MSI based installation file.")] + [ValidateNotNullOrEmpty()] + [System.IO.FileInfo]$Path, + + [parameter(Mandatory = $true, HelpMessage = "Specify the MSI database property to retrieve it's value.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("ProductCode", "ProductVersion", "ProductName", "Manufacturer", "ProductLanguage", "FullVersion")] + [string]$Property + ) + Process { + try { + # Read property from MSI database + $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer + $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0)) + $Query = "SELECT Value FROM Property WHERE Property = '$($Property)'" + $View = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, ($Query)) + $View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null) + $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null) + $Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1) + + # Commit database and close view + $MSIDatabase.GetType().InvokeMember("Commit", "InvokeMethod", $null, $MSIDatabase, $null) + $View.GetType().InvokeMember("Close", "InvokeMethod", $null, $View, $null) + $MSIDatabase = $null + $View = $null + + # Return the value + return $Value + } + catch { + Write-Warning -Message $_.Exception.Message; break + } + } + End { + # Run garbage collection and release ComObject + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($WindowsInstaller) | Out-Null + [System.GC]::Collect() + } +} + +function New-IntuneWin32AppPackage { + <# + .SYNOPSIS + Package an application as a Win32 application container (.intunewin) for usage with Microsoft Intune. + + .DESCRIPTION + Package an application as a Win32 application container (.intunewin) for usage with Microsoft Intune. + + .PARAMETER SourceFolder + Specify the full path of the source folder where the setup file and all of it's potential dependency files reside. + + .PARAMETER SetupFile + Specify the complete setup file name including it's file extension, e.g. Setup.exe or Installer.msi. + + .PARAMETER OutputFolder + Specify the full path of the output folder where the packaged .intunewin file will be exported to. + + .PARAMETER IntuneWinAppUtilPath + Specify the full path to the IntuneWinAppUtil.exe file. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [parameter(Mandatory = $true, HelpMessage = "Specify the full path of the source folder where the setup file and all of it's potential dependency files reside.")] + [ValidateNotNullOrEmpty()] + [string]$SourceFolder, + + [parameter(Mandatory = $true, HelpMessage = "Specify the complete setup file name including it's file extension, e.g. Setup.exe or Installer.msi.")] + [ValidateNotNullOrEmpty()] + [string]$SetupFile, + + [parameter(Mandatory = $true, HelpMessage = "Specify the full path of the output folder where the packaged .intunewin file will be exported to.")] + [ValidateNotNullOrEmpty()] + [string]$OutputFolder, + + [parameter(Mandatory = $false, HelpMessage = "Specify the full path to the IntuneWinAppUtil.exe file.")] + [ValidateNotNullOrEmpty()] + [string]$IntuneWinAppUtilPath = (Join-Path -Path $env:TEMP -ChildPath "IntuneWinAppUtil.exe") + ) + Process { + if (Test-Path -Path $SourceFolder) { + Write-Verbose -Message "Successfully detected specified source folder: $($SourceFolder)" + + if (Test-Path -Path (Join-Path -Path $SourceFolder -ChildPath $SetupFile)) { + Write-Verbose -Message "Successfully detected specified setup file '$($SetupFile)' in source folder" + + if (Test-Path -Path $OutputFolder) { + Write-Verbose -Message "Successfully detected specified output folder: $($OutputFolder)" + + if (-not(Test-Path -Path $IntuneWinAppUtilPath)) { + if (-not($PSBoundParameters["IntuneWinAppUtilPath"])) { + # Download IntuneWinAppUtil.exe if not present in context temporary folder + Write-Verbose -Message "Unable to detect IntuneWinAppUtil.exe in specified location, attempting to download to: $($env:TEMP)" + Start-DownloadFile -URL "https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool/raw/master/IntuneWinAppUtil.exe" -Path $env:TEMP -Name "IntuneWinAppUtil.exe" + + # Override path for IntuneWinApputil.exe if custom path was passed as a parameter, but was not found and downloaded to temporary location + $IntuneWinAppUtilPath = Join-Path -Path $env:TEMP -ChildPath "IntuneWinAppUtil.exe" + } + } + + if (Test-Path -Path $IntuneWinAppUtilPath) { + Write-Verbose -Message "Successfully detected IntuneWinAppUtil.exe in: $($IntuneWinAppUtilPath)" + + # Invoke IntuneWinAppUtil.exe with parameter inputs + $PackageInvocation = Invoke-Executable -FilePath $IntuneWinAppUtilPath -Arguments "-c ""$($SourceFolder)"" -s ""$($SetupFile)"" -o ""$($OutPutFolder)""" # -q + if ($PackageInvocation -eq 0) { + $IntuneWinAppPackage = Join-Path -Path $OutputFolder -ChildPath "$([System.IO.Path]::GetFileNameWithoutExtension($SetupFile)).intunewin" + if (Test-Path -Path $IntuneWinAppPackage) { + Write-Verbose -Message "Successfully created Win32 app package object" + + # Retrieve Win32 app package meta data + $IntuneWinAppMetaData = Get-IntuneWin32AppMetaData -FilePath $IntuneWinAppPackage + + # Construct output object with package details + $PSObject = [PSCustomObject]@{ + "Name" = $IntuneWinAppMetaData.ApplicationInfo.Name + "FileName" = $IntuneWinAppMetaData.ApplicationInfo.FileName + "SetupFile" = $IntuneWinAppMetaData.ApplicationInfo.SetupFile + "UnencryptedContentSize" = $IntuneWinAppMetaData.ApplicationInfo.UnencryptedContentSize + "Path" = $IntuneWinAppPackage + } + Write-Output -InputObject $PSObject + } + else { + Write-Warning -Message "Unable to detect expected '$($SetupFile).intunewin' file after IntuneWinAppUtil.exe invocation" + } + } + else { + Write-Warning -Message "Unexpect error occurred while packaging Win32 app. Return code from invocation: $($PackageInvocation)" + } + } + else { + Write-Warning -Message "Unable to detect IntuneWinAppUtil.exe in: $($IntuneWinAppUtilPath)" + } + } + else { + Write-Warning -Message "Unable to detect specified output folder: $($OutputFolder)" + } + } + else { + Write-Warning -Message "Unable to detect specified setup file '$($SetupFile)' in source folder: $($SourceFolder)" + } + } + else { + Write-Warning -Message "Unable to detect specified source folder: $($SourceFolder)" + } + } +} + +function Get-IntuneWin32App { + <# + .SYNOPSIS + Get all or a specific Win32 app by either DisplayName or ID. + + .DESCRIPTION + Get all or a specific Win32 app by either DisplayName or ID. + + .PARAMETER TenantName + Specify the tenant name, e.g. domain.onmicrosoft.com. + + .PARAMETER DisplayName + Specify the display name for a Win32 application. + + .PARAMETER ID + Specify the ID for a Win32 application. + + .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. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-20 + + Version history: + 1.0.0 - (2020-01-04) Function created + 1.0.1 - (2020-01-20) Updated to load all properties for objects return and support multiple objects returned for wildcard search when specifying display name + #> + [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Default")] + param( + [parameter(Mandatory = $true, ParameterSetName = "Default", HelpMessage = "Specify the tenant name, e.g. domain.onmicrosoft.com.")] + [parameter(Mandatory = $true, ParameterSetName = "DisplayName")] + [parameter(Mandatory = $true, ParameterSetName = "ID")] + [ValidateNotNullOrEmpty()] + [string]$TenantName, + + [parameter(Mandatory = $true, ParameterSetName = "DisplayName", HelpMessage = "Specify the display name for a Win32 application.")] + [ValidateNotNullOrEmpty()] + [string]$DisplayName, + + [parameter(Mandatory = $true, ParameterSetName = "ID", HelpMessage = "Specify the ID for a Win32 application.")] + [ValidateNotNullOrEmpty()] + [string]$ID, + + [parameter(Mandatory = $false, ParameterSetName = "Default", 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.")] + [parameter(Mandatory = $false, ParameterSetName = "DisplayName")] + [parameter(Mandatory = $false, ParameterSetName = "ID")] + [ValidateNotNullOrEmpty()] + [string]$ApplicationID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547", + + [parameter(Mandatory = $false, ParameterSetName = "Default", HelpMessage = "Set the prompt behavior when acquiring a token.")] + [parameter(Mandatory = $false, ParameterSetName = "DisplayName")] + [parameter(Mandatory = $false, ParameterSetName = "ID")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Auto", "Always", "Never", "RefreshSession")] + [string]$PromptBehavior = "Auto" + ) + Begin { + # Ensure required auth token exists or retrieve a new one + Get-AuthToken -TenantName $TenantName -ApplicationID $ApplicationID -PromptBehavior $PromptBehavior + } + Process { + switch ($PSCmdlet.ParameterSetName) { + "DisplayName" { + Write-Verbose -Message "Attempting to retrieve all mobileApps resources to determine ID of Win32 app" + $Win32AppList = New-Object -TypeName System.Collections.ArrayList + $MobileApps = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps" -Method "GET" + if ($MobileApps.value.Count -ge 1) { + Write-Verbose -Message "Filtering query response for mobileApps matching type '#microsoft.graph.win32LobApp'" + $Win32MobileApps = $MobileApps.value | Where-Object { $_.'@odata.type' -like "#microsoft.graph.win32LobApp" } + if ($Win32MobileApps -ne $null) { + Write-Verbose -Message "Filtering for Win32 apps matching displayName: $($DisplayName)" + $Win32MobileApps = $Win32MobileApps | Where-Object { $_.displayName -like "*$($DisplayName)*" } + if ($Win32MobileApps -ne $null) { + foreach ($Win32MobileApp in $Win32MobileApps) { + Write-Verbose -Message "Querying for Win32 app using ID: $($Win32MobileApp.id)" + $Win32App = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps/$($Win32MobileApp.id)" -Method "GET" + $Win32AppList.Add($Win32App) | Out-Null + } + + # Handle return value + return $Win32AppList + } + else { + Write-Warning -Message "Query for Win32 app returned an empty result, no apps matching the specified search criteria was found" + } + } + else { + Write-Warning -Message "Query for Win32 apps returned an empty result, no apps matching type 'win32LobApp' was found in tenant" + } + } + } + "ID" { + Write-Verbose -Message "Querying for Win32 apps matching id: $($ID)" + $Win32App = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps/$($ID)" -Method "GET" + + # Handle return value + return $Win32App + } + default { + Write-Verbose -Message "Querying for all Win32 apps" + $Win32AppList = New-Object -TypeName System.Collections.ArrayList + $Win32MobileApps = (Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps?`$filter=isof('microsoft.graph.win32LobApp')" -Method "GET").value + if ($Win32MobileApps.Count -ge 1) { + foreach ($Win32MobileApp in $Win32MobileApps) { + Write-Verbose -Message "Querying explicitly to retrieve all properties for Win32 app with ID: $($Win32MobileApp.id)" + $Win32App = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps/$($Win32MobileApp.id)" -Method "GET" + $Win32AppList.Add($Win32App) | Out-Null + } + + # Handle return value + return $Win32AppList + } + else { + Write-Warning -Message "Query for Win32 apps returned an empty result, no apps matching type 'win32LobApp' was found in tenant" + } + } + } + } +} + +function Add-IntuneWin32AppAssignment { +<# + .SYNOPSIS + Add an assignment to a Win32 app. + + .DESCRIPTION + Add an assignment to a Win32 app. + + .PARAMETER TenantName + Specify the tenant name, e.g. domain.onmicrosoft.com. + + .PARAMETER DisplayName + Specify the display name for a Win32 application. + + .PARAMETER ID + Specify the ID for a Win32 application. + + .PARAMETER Target + Specify the target of the assignment, either AllUsers or Group. + + .PARAMETER Intent + Specify the intent of the assignment, either required or available. + + .PARAMETER GroupID + Specify the ID for an Azure AD group. + + .PARAMETER Notification + Specify the notification setting for the assignment of the Win32 app. + + .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. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [parameter(Mandatory = $true, ParameterSetName = "DisplayName", HelpMessage = "Specify the tenant name, e.g. domain.onmicrosoft.com.")] + [parameter(Mandatory = $true, ParameterSetName = "ID")] + [ValidateNotNullOrEmpty()] + [string]$TenantName, + + [parameter(Mandatory = $true, ParameterSetName = "DisplayName", HelpMessage = "Specify the display name for a Win32 application.")] + [ValidateNotNullOrEmpty()] + [string]$DisplayName, + + [parameter(Mandatory = $true, ParameterSetName = "ID", HelpMessage = "Specify the ID for a Win32 application.")] + [ValidateNotNullOrEmpty()] + [string]$ID, + + [parameter(Mandatory = $true, ParameterSetName = "DisplayName", HelpMessage = "Specify the target of the assignment, either AllUsers or Group.")] + [parameter(Mandatory = $true, ParameterSetName = "ID")] + [ValidateNotNullOrEmpty()] + [ValidateSet("AllUsers", "Group")] + [string]$Target, + + [parameter(Mandatory = $false, ParameterSetName = "DisplayName", HelpMessage = "Specify the intent of the assignment, either required or available.")] + [parameter(Mandatory = $false, ParameterSetName = "ID")] + [ValidateNotNullOrEmpty()] + [ValidateSet("required", "available")] + [string]$Intent = "available", + + [parameter(Mandatory = $false, ParameterSetName = "DisplayName", HelpMessage = "Specify the ID for an Azure AD group.")] + [parameter(Mandatory = $false, ParameterSetName = "ID")] + [ValidateNotNullOrEmpty()] + [string]$GroupID, + + [parameter(Mandatory = $false, ParameterSetName = "DisplayName", HelpMessage = "Specify the notification setting for the assignment of the Win32 app.")] + [parameter(Mandatory = $false, ParameterSetName = "ID")] + [ValidateNotNullOrEmpty()] + [ValidateSet("showAll", "showReboot", "hideAll")] + [string]$Notification = "showAll", + + [parameter(Mandatory = $false, ParameterSetName = "DisplayName", 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.")] + [parameter(Mandatory = $false, ParameterSetName = "ID")] + [ValidateNotNullOrEmpty()] + [string]$ApplicationID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547", + + [parameter(Mandatory = $false, ParameterSetName = "DisplayName", HelpMessage = "Set the prompt behavior when acquiring a token.")] + [parameter(Mandatory = $false, ParameterSetName = "ID")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Auto", "Always", "Never", "RefreshSession")] + [string]$PromptBehavior = "Auto" + ) + Begin { + # Ensure required auth token exists or retrieve a new one + Get-AuthToken -TenantName $TenantName -ApplicationID $ApplicationID -PromptBehavior $PromptBehavior + + # Validate group identifier is passed as input if target is set to Group + if ($Target -like "Group") { + if (-not($PSBoundParameters["GroupID"])) { + Write-Warning -Message "Validation failed for parameter input, target set to Group but GroupID parameter was not specified" + } + } + } + Process { + switch ($PSCmdlet.ParameterSetName) { + "DisplayName" { + $MobileApps = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps" -Method "GET" + if ($MobileApps.value.Count -ge 1) { + $Win32MobileApps = $MobileApps.value | Where-Object { $_.'@odata.type' -like "#microsoft.graph.win32LobApp" } + if ($Win32MobileApps -ne $null) { + $Win32App = $Win32MobileApps | Where-Object { $_.displayName -like $DisplayName } + if ($Win32App -ne $null) { + Write-Verbose -Message "Detected Win32 app with ID: $($Win32App.id)" + $Win32AppID = $Win32App.id + } + else { + Write-Warning -Message "Query for Win32 apps returned empty a result, no apps matching the specified search criteria was found" + } + } + else { + Write-Warning -Message "Query for Win32 apps returned empty a result, no apps matching type 'win32LobApp' was found in tenant" + } + } + else { + Write-Warning -Message "Query for mobileApps resources returned empty" + } + } + "ID" { + $Win32AppID = $ID + } + } + + if (-not([string]::IsNullOrEmpty($Win32AppID))) { + # Determine target property body based on parameter input + switch ($Target) { + "AllUsers" { + $TargetAssignment = @{ + "@odata.type" = "#microsoft.graph.allLicensedUsersAssignmentTarget" + } + } + "Group" { + $TargetAssignment = @{ + "@odata.type" = "#microsoft.graph.groupAssignmentTarget" + "groupId" = $GroupID + } + } + } + + # Construct table for Win32 app assignment body + $Win32AppAssignmentBody = [ordered]@{ + "@odata.type" = "#microsoft.graph.mobileAppAssignment" + "intent" = $Intent + "source" = "direct" + "target" = $TargetAssignment + "settings" = @{ + "@odata.type" = "#microsoft.graph.win32LobAppAssignmentSettings" + "notifications" = $Notification + "restartSettings" = $null + "installTimeSettings" = $null + } + } + + try { + # Attempt to call Graph and create new assignment for Win32 app + $Win32AppAssignmentResponse = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps/$($Win32AppID)/assignments" -Method "POST" -Body ($Win32AppAssignmentBody | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop + if ($Win32AppAssignmentResponse.id) { + Write-Verbose -Message "Successfully created Win32 app assignment with ID: $($Win32AppAssignmentResponse.id)" + Write-Output -InputObject $Win32AppAssignmentResponse + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while creating a CryptoStream and writing decoded chunks of data to file: $($TargetFilePath). Error message: $($_.Exception.Message)" + } + } + else { + Write-Warning -Message "Unable to determine the Win32 app identification for assignment" + } + } +} + +function Expand-IntuneWin32AppPackage { + <# + .SYNOPSIS + Decode an existing .intunewin file already packaged as a Win32 application and allow it's contents to be extracted. + + .DESCRIPTION + Decode an existing .intunewin file already packaged as a Win32 application and allow it's contents to be extracted. + + .PARAMETER FilePath + Specify the full path of the locally available packaged Win32 application, e.g. 'C:\Temp\AppName.intunewin'. + + .PARAMETER Force + Specify parameter to overwrite existing files already in working directory. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [parameter(Mandatory = $true, HelpMessage = "Specify the full path of the locally available packaged Win32 application, e.g. 'C:\Temp\AppName.intunewin'.")] + [ValidateNotNullOrEmpty()] + [ValidatePattern("^[A-Za-z]{1}:\\\w+\\\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"; break + } + else { + # Check if file extension is intunewin + if ([System.IO.Path]::GetExtension((Split-Path -Path $_ -Leaf)) -like ".intunewin") { + return $true + } + else { + Write-Warning -Message "$(Split-Path -Path $_ -Leaf) contains unsupported file extension. Supported extension is '.intunewin'"; break + } + } + })] + [string]$FilePath, + + [parameter(Mandatory = $false, HelpMessage = "Specify parameter to overwrite existing files already in working directory.")] + [switch]$Force + ) + Begin { + # Load System.IO.Compression assembly for managing compressed files + try { + $ClassImport = Add-Type -AssemblyName "System.IO.Compression.FileSystem" -ErrorAction Stop -Verbose:$false + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while loading System.IO.Compression.FileSystem assembly. Error message: $($_.Exception.Message)"; break + } + + # Set script variable for error action preference + $ErrorActionPreference = "Stop" + } + Process { + if (Test-Path -Path $FilePath) { + try { + # Read Win32 app meta data + Write-Verbose -Message "Attempting to gather required Win32 app meta data from file: $($FilePath)" + $IntuneWinMetaData = Get-IntuneWin32AppMetaData -FilePath $FilePath -ErrorAction Stop + if ($IntuneWinMetaData -ne $null) { + # Retrieve Base64 encoded encryption key + $Base64Key = $IntuneWinMetaData.ApplicationInfo.EncryptionInfo.EncryptionKey + Write-Verbose -Message "Found Base64 encoded encryption key from meta data: $($Base64Key)" + + # Retrieve Base64 encoded initialization vector + $Base64IV = $IntuneWinMetaData.ApplicationInfo.EncryptionInfo.InitializationVector + Write-Verbose -Message "Found Base64 encoded initialization vector from meta data: $($Base64IV)" + + try { + # Extract encoded .intunewin from Contents folder + Write-Verbose -Message "Attempting to extract encoded .intunewin file from inside Contents folder of the Win32 application package" + $ExtractedIntuneWinFile = $FilePath + ".extracted" + $ZipFile = [System.IO.Compression.ZipFile]::OpenRead($IntuneWinFile) + $IntuneWinFileName = Split-Path -Path $FilePath -Leaf + $ZipFile.Entries | Where-Object { $_.Name -like $IntuneWinFileName } | ForEach-Object { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $ExtractedIntuneWinFile, $true) + } + + # Dispose of ZipFile from memory + $ZipFile.Dispose() + + try { + # Convert Base64 encryption info to bytes + Write-Verbose -Message "Attempting to convert Base64 encoded encryption key and initialization vector secure strings" + $Key = [System.Convert]::FromBase64String($Base64Key) + $IV = [System.Convert]::FromBase64String($Base64IV) + + try { + # Open target filestream for read/write + $TargetFilePath = $FilePath + ".decoded" + $TargetFilePathName = Split-Path -Path $TargetFilePath -Leaf + if (Test-Path -Path $TargetFilePath) { + if ($PSBoundParameters["Force"]) { + try { + Remove-Item -Path $TargetFilePath -Force -ErrorAction Stop + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while removing existing decoded file: $($TargetFilePathName). Error message: $($_.Exception.Message)"; break + } + } + else { + Write-Warning -Message "Existing file '$($TargetFilePathName)' already exists, use Force parameter to overwrite"; break + } + } + + Write-Verbose -Message "Attempting to create a new decoded .intunewin file: $($TargetFilePath)" + [System.IO.FileStream]$FileStreamTarget = [System.IO.File]::Open($TargetFilePath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) + + try { + # Create AES decryptor + Write-Verbose -Message "Attempting to construct new AES decryptor with encryption key and initialization vector" + $AES = [System.Security.Cryptography.Aes]::Create() + [System.Security.Cryptography.ICryptoTransform]$Decryptor = $AES.CreateDecryptor($Key, $IV) + + try { + # Open source filestream for read-only + Write-Verbose -Message "Attepmting to open extracted .intunewin file: $($ExtractedIntuneWinFile)" + [System.IO.FileStream]$FileStreamSource = [System.IO.File]::Open($ExtractedIntuneWinFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::None) + $FileStreamSourceSeek = $FileStreamSource.Seek(48l, [System.IO.SeekOrigin]::Begin) + + try { + # Construct new CryptoStream + Write-Verbose -Message "Attempting to create CryptoStream and write decoded chunks of data to file: $($TargetFilePath)" + [System.Security.Cryptography.CryptoStream]$CryptoStream = New-Object -TypeName System.Security.Cryptography.CryptoStream -ArgumentList @($FileStreamTarget, $Decryptor, [System.Security.Cryptography.CryptoStreamMode]::Write) -ErrorAction Stop + + # Write all chunks of data to decoded target file + $buffer = New-Object byte[](2097152) + while ($BytesRead = $FileStreamSource.Read($buffer, 0, 2097152)) { + $CryptoStream.Write($buffer, 0, $BytesRead) + $CryptoStream.Flush() + } + + # Flush final block in cryptostream + $CryptoStream.FlushFinalBlock() + Write-Verbose -Message "Successfully decoded '$($IntuneWinFileName)' Win32 app package file to: $($TargetFilePath)" + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while creating a CryptoStream and writing decoded chunks of data to file: $($TargetFilePath). Error message: $($_.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while opening extracted .intunewin file '$($ExtractedIntuneWinFile)'. Error message: $($_.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while creating AES decryptor. Error message: $($_.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while creating a new decoded .intunewin file: $($TargetFilePath). Error message: $($_.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while converting Base64 encoded encryption key and initialization vector secure strings. Error message: $($_.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while extracing encoded .intunewin file from inside Contents folder of the Win32 application package. Error message: $($_.Exception.Message)" + } + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while gathering Win32 app meta data. Error message: $($_.Exception.Message)" + } + } + else { + Write-Warning -Message "Unable to locate specified .intunewin file" + } + } + End { + # Dispose of objects and release locks + if ($CryptoStream -ne $null) { + $CryptoStream.Dispose() + } + if ($FileStreamSource -ne $null) { + $FileStreamSource.Dispose() + } + if ($Decryptor -ne $null) { + $Decryptor.Dispose() + } + if ($FileStreamTarget -ne $null) { + $FileStreamTarget.Dispose() + } + if ($AES -ne $null) { + $AES.Dispose() + } + + # Remove extracted intunewin file + if (Test-Path -Path $ExtractedIntuneWinFile) { + Remove-Item -Path $ExtractedIntuneWinFile -Force + } + } +} + +function Add-IntuneWin32App { + <# + .SYNOPSIS + Create a new Win32 application in Microsoft Intune. + + .DESCRIPTION + Create a new Win32 application in Microsoft Intune. + + .PARAMETER TenantName + Specify the tenant name, e.g. domain.onmicrosoft.com. + + .PARAMETER FilePath + Specify a local path to where the win32 app .intunewin file is located. + + .PARAMETER DisplayName + Specify a display name for the Win32 application. + + .PARAMETER Description + Specify a description for the Win32 application. + + .PARAMETER Publisher + Specify a publisher name for the Win32 application. + + .PARAMETER Developer + Specify the developer name for the Win32 application. + + .PARAMETER InstallCommandLine + Specify the install command line for the Win32 application. + + .PARAMETER UninstallCommandLine + Specify the uninstall command line for the Win32 application. + + .PARAMETER InstallExperience + Specify the install experience for the Win32 application. Supported values are: system or user. + + .PARAMETER RestartBehavior + Specify the restart behavior for the Win32 application. Supported values are: allow, basedOnReturnCode, suppress or force. + + .PARAMETER DetectionRule + Provide an array of a single or multiple OrderedDictionary objects as detection rules that will be used for the Win32 application. + + .PARAMETER RequirementRule + Provide an OrderedDictionary object as requirement rule that will be used for the Win32 application. + + .PARAMETER ReturnCode + Provide an array of a single or multiple hash-tables for the Win32 application with return code information. + + .PARAMETER Icon + Provide a Base64 encoded string of the PNG/JPG/JPEG file. + + .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. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + 1.0.1 - (2020-01-27) Added support for RequirementRule parameter input + + Required modules: + AzureAD (Install-Module -Name AzureAD) + PSIntuneAuth (Install-Module -Name PSIntuneAuth) + #> + [CmdletBinding(SupportsShouldProcess=$true, DefaultParameterSetName = "MSI")] + param( + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the tenant name, e.g. domain.onmicrosoft.com.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$TenantName, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify a local path to where the win32 app .intunewin file is located.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [ValidatePattern("^[A-Za-z]{1}:\\\w+\\\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"; break + } + else { + # Check if file extension is intunewin + if ([System.IO.Path]::GetExtension((Split-Path -Path $_ -Leaf)) -like ".intunewin") { + return $true + } + else { + Write-Warning -Message "$(Split-Path -Path $_ -Leaf) contains unsupported file extension. Supported extension is '.intunewin'"; break + } + } + })] + [string]$FilePath, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Specify a display name for the Win32 application.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$DisplayName, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Specify a description for the Win32 application.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$Description, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Specify a publisher name for the Win32 application.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$Publisher, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Specify the developer name for the Win32 application.")] + [parameter(Mandatory = $false, ParameterSetName = "EXE")] + [string]$Developer = [string]::Empty, + + [parameter(Mandatory = $true, ParameterSetName = "EXE", HelpMessage = "Specify the install command line for the Win32 application.")] + [ValidateNotNullOrEmpty()] + [string]$InstallCommandLine, + + [parameter(Mandatory = $true, ParameterSetName = "EXE", HelpMessage = "Specify the uninstall command line for the Win32 application.")] + [ValidateNotNullOrEmpty()] + [string]$UninstallCommandLine, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the install experience for the Win32 application. Supported values are: system or user.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [ValidateSet("system", "user")] + [string]$InstallExperience, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the restart behavior for the Win32 application. Supported values are: allow, basedOnReturnCode, suppress or force.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [ValidateSet("allow", "basedOnReturnCode", "suppress", "force")] + [string]$RestartBehavior, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Provide an array of a single or multiple OrderedDictionary objects as detection rules that will be used for the Win32 application.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [System.Collections.Specialized.OrderedDictionary[]]$DetectionRule, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Provide an OrderedDictionary object as requirement rule that will be used for the Win32 application.")] + [parameter(Mandatory = $false, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [System.Collections.Specialized.OrderedDictionary]$RequirementRule, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Provide an array of a single or multiple hash-tables for the Win32 application with return code information.")] + [parameter(Mandatory = $false, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [System.Collections.Hashtable[]]$ReturnCode, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Provide a Base64 encoded string of the PNG/JPG/JPEG file.")] + [parameter(Mandatory = $false, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$Icon, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", 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.")] + [parameter(Mandatory = $false, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$ApplicationID = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547", + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Set the prompt behavior when acquiring a token.")] + [parameter(Mandatory = $false, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [ValidateSet("Auto", "Always", "Never", "RefreshSession")] + [string]$PromptBehavior = "Auto" + ) + Begin { + # Ensure required auth token exists or retrieve a new one + Get-AuthToken -TenantName $TenantName -ApplicationID $ApplicationID -PromptBehavior $PromptBehavior + + # Set script variable for error action preference + $ErrorActionPreference = "Stop" + } + Process { + try { + # Attempt to gather all possible meta data from specified .intunewin file + Write-Verbose -Message "Attempting to gather additional meta data from .intunewin file: $($FilePath)" + $IntuneWinXMLMetaData = Get-IntuneWin32AppMetaData -FilePath $FilePath -ErrorAction Stop + + if ($IntuneWinXMLMetaData -ne $null) { + Write-Verbose -Message "Successfully gathered additional meta data from .intunewin file" + + # Generate Win32 application body data table with different parameters based upon parameter set name + Write-Verbose -Message "Start constructing basic layout of Win32 app body" + switch ($PSCmdlet.ParameterSetName) { + "MSI" { + # Determine the execution context of the MSI installer and define the installation purpose + $MSIExecutionContext = $IntuneWinXMLMetaData.ApplicationInfo.MsiInfo.MsiExecutionContext + $MSIInstallPurpose = "DualPurpose" + switch ($MSIExecutionContext) { + "System" { + $MSIInstallPurpose = "PerMachine" + } + "User" { + $MSIInstallPurpose = "PerUser" + } + } + + # Handle special meta data variable values + $MSIRequiresReboot = $IntuneWinXMLMetaData.ApplicationInfo.MsiInfo.MsiRequiresReboot + switch ($MSIRequiresReboot) { + "true" { + $MSIRequiresReboot = $true + } + "false" { + $MSIRequiresReboot = $false + } + } + + # Handle special parameter inputs + if (-not($PSBoundParameters["DisplayName"])) { + $DisplayName = $IntuneWinXMLMetaData.ApplicationInfo.Name + } + if (-not($PSBoundParameters["Description"])) { + $Description = $IntuneWinXMLMetaData.ApplicationInfo.Name + } + if (-not($PSBoundParameters["Publisher"])) { + $Publisher = $IntuneWinXMLMetaData.ApplicationInfo.MsiInfo.MsiPublisher + } + if (-not($PSBoundParameters["Developer"])) { + $Developer = [string]::Empty + } + + # Generate Win32 application body + $AppBodySplat = @{ + "MSI" = $true + "DisplayName" = $DisplayName + "Description" = $Description + "Publisher" = $Publisher + "Developer" = $Developer + "FileName" = $IntuneWinXMLMetaData.ApplicationInfo.FileName + "SetupFileName" = $IntuneWinXMLMetaData.ApplicationInfo.SetupFile + "InstallExperience" = $InstallExperience + "RestartBehavior" = $RestartBehavior + "MSIInstallPurpose" = $MSIInstallPurpose + "MSIProductCode" = $IntuneWinXMLMetaData.ApplicationInfo.MsiInfo.MsiProductCode + "MSIProductName" = $DisplayName + "MSIProductVersion" = $IntuneWinXMLMetaData.ApplicationInfo.MsiInfo.MsiProductVersion + "MSIRequiresReboot" = $MSIRequiresReboot + "MSIUpgradeCode" = $IntuneWinXMLMetaData.ApplicationInfo.MsiInfo.MsiUpgradeCode + } + if ($PSBoundParameters["Icon"]) { + $AppBodySplat.Add("Icon", $Icon) + } + if ($PSBoundParameters["RequirementRule"]) { + $AppBodySplat.Add("RequirementRule", $RequirementRule) + } + + $Win32AppBody = New-IntuneWin32AppBody @AppBodySplat + Write-Verbose -Message "Constructed the basic layout for 'MSI' Win32 app body type" + } + "EXE" { + # Generate Win32 application body + $AppBodySplat = @{ + "EXE" = $true + "DisplayName" = $DisplayName + "Description" = $Description + "Publisher" = $Publisher + "Developer" = $Developer + "FileName" = $IntuneWinXMLMetaData.ApplicationInfo.FileName + "SetupFileName" = $IntuneWinXMLMetaData.ApplicationInfo.SetupFile + "InstallExperience" = $InstallExperience + "RestartBehavior" = $RestartBehavior + "InstallCommandLine" = $InstallCommandLine + "UninstallCommandLine" = $UninstallCommandLine + } + if ($PSBoundParameters["Icon"]) { + $AppBodySplat.Add("Icon", $Icon) + } + if ($PSBoundParameters["RequirementRule"]) { + $AppBodySplat.Add("RequirementRule", $RequirementRule) + } + + $Win32AppBody = New-IntuneWin32AppBody @AppBodySplat + Write-Verbose -Message "Constructed the basic layout for 'EXE' Win32 app body type" + } + } + + # Validate that correct detection rules have been passed on command line, only 1 PowerShell script based detection rule is allowed + if (($DetectionRule.'@odata.type' -contains "#microsoft.graph.win32LobAppPowerShellScriptDetection") -and (@($DetectionRules).'@odata.type'.Count -gt 1)) { + Write-Warning -Message "Multiple PowerShell Script detection rules were detected, this is not a supported configuration"; break + } + else { + # Add detection rules to Win32 app body object + Write-Verbose -Message "Detection rule objects passed validation checks, attempting to add to existing Win32 app body" + $Win32AppBody.Add("detectionRules", $DetectionRule) + + # Retrieve the default return codes for a Win32 app + Write-Verbose -Message "Retrieving default set of return codes for Win32 app body construction" + $DefaultReturnCodes = Get-IntuneWin32AppDefaultReturnCode + + # Add custom return codes from parameter input to default set of objects + if ($PSBoundParameters["ReturnCode"]) { + Write-Verbose -Message "Additional return codes where passed as command line input, adding to array of default return codes" + foreach ($ReturnCodeItem in $ReturnCode) { + $DefaultReturnCodes += $ReturnCodeItem + } + } + + # Add return codes to Win32 app body object + Write-Verbose -Message "Adding array of return codes to Win32 app body construction" + $Win32AppBody.Add("returnCodes", $DefaultReturnCodes) + + # + ## Placeholder for adding requirement rules here + # + + # Create the Win32 app + Write-Verbose -Message "Attempting to create Win32 app using constructed body converted to JSON content" + $Win32MobileAppRequest = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps" -Method "POST" -Body ($Win32AppBody | ConvertTo-Json) + if ($Win32MobileAppRequest.'@odata.type' -notlike "#microsoft.graph.win32LobApp") { + Write-Warning -Message "Failed to create Win32 app using constructed body. Passing converted body as JSON to output."; break + Write-Output -InputObject ($Win32AppBody | ConvertTo-Json) + } + else { + Write-Verbose -Message "Successfully created Win32 app with ID: $($Win32MobileAppRequest.id)" + + # Create Content Version for the Win32 app + Write-Verbose -Message "Attempting to create contentVersions resource for the Win32 app" + $Win32MobileAppContentVersionRequest = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps/$($Win32MobileAppRequest.id)/microsoft.graph.win32LobApp/contentVersions" -Method "POST" -Body "{}" + if ([string]::IsNullOrEmpty($Win32MobileAppContentVersionRequest.id)) { + Write-Warning -Message "Failed to create contentVersions resource for Win32 app"; break + } + else { + Write-Verbose -Message "Successfully created contentVersions resource with ID: $($Win32MobileAppContentVersionRequest.id)" + + # Extract compressed .intunewin file to subfolder + $IntuneWinFilePath = Expand-IntuneWin32AppCompressedFile -FilePath $FilePath -FileName $IntuneWinXMLMetaData.ApplicationInfo.FileName -FolderName ($IntuneWinXMLMetaData.ApplicationInfo.Name).Replace(".intunewin", "") + if ($IntuneWinFilePath -ne $null) { + # Create a new file entry in Intune for the upload of the .intunewin file + Write-Verbose -Message "Constructing Win32 app content file body for uploading of .intunewin file" + $Win32AppFileBody = [ordered]@{ + "@odata.type" = "#microsoft.graph.mobileAppContentFile" + "name" = $IntuneWinXMLMetaData.ApplicationInfo.FileName + "size" = [int64]$IntuneWinXMLMetaData.ApplicationInfo.UnencryptedContentSize + "sizeEncrypted" = (Get-Item -Path $IntuneWinFilePath).Length + "manifest" = $null + "isDependency" = $false + } + + # Create the contentVersions files resource + $Win32MobileAppFileContentRequest = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps/$($Win32MobileAppRequest.id)/microsoft.graph.win32LobApp/contentVersions/$($Win32MobileAppContentVersionRequest.id)/files" -Method "POST" -Body ($Win32AppFileBody | ConvertTo-Json) + if ([string]::IsNullOrEmpty($Win32MobileAppFileContentRequest.id)) { + Write-Warning -Message "Failed to create Azure Storage blob for contentVersions/files resource for Win32 app"; break + } + else { + # Wait for the Win32 app file content URI to be created + Write-Verbose -Message "Waiting for Intune service to process contentVersions/files request" + $FilesUri = "mobileApps/$($Win32MobileAppRequest.id)/microsoft.graph.win32LobApp/contentVersions/$($Win32MobileAppContentVersionRequest.id)/files/$($Win32MobileAppFileContentRequest.id)" + $ContentVersionsFiles = Wait-IntuneWin32AppFileProcessing -Stage "AzureStorageUriRequest" -Resource $FilesUri + + # Upload .intunewin file to Azure Storage blob + Invoke-AzureStorageBlobUpload -StorageUri $ContentVersionsFiles.azureStorageUri -FilePath $IntuneWinFilePath -Resource $FilesUri + + # Retrieve encryption meta data from .intunewin file + $IntuneWinEncryptionInfo = [ordered]@{ + "encryptionKey" = $IntuneWinXMLMetaData.ApplicationInfo.EncryptionInfo.EncryptionKey + "macKey" = $IntuneWinXMLMetaData.ApplicationInfo.EncryptionInfo.macKey + "initializationVector" = $IntuneWinXMLMetaData.ApplicationInfo.EncryptionInfo.initializationVector + "mac" = $IntuneWinXMLMetaData.ApplicationInfo.EncryptionInfo.mac + "profileIdentifier" = "ProfileVersion1" + "fileDigest" = $IntuneWinXMLMetaData.ApplicationInfo.EncryptionInfo.fileDigest + "fileDigestAlgorithm" = $IntuneWinXMLMetaData.ApplicationInfo.EncryptionInfo.fileDigestAlgorithm + } + $IntuneWinFileEncryptionInfo = @{ + "fileEncryptionInfo" = $IntuneWinEncryptionInfo + } + + # Create file commit request + $CommitResource = "mobileApps/$($Win32MobileAppRequest.id)/microsoft.graph.win32LobApp/contentVersions/$($Win32MobileAppContentVersionRequest.id)/files/$($Win32MobileAppFileContentRequest.id)/commit" + $Win32AppFileCommitRequest = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource $CommitResource -Method "POST" -Body ($IntuneWinFileEncryptionInfo | ConvertTo-Json) + + # Wait for Intune service to process the commit file request + Write-Verbose -Message "Waiting for Intune service to process the commit file request" + $CommitFileRequest = Wait-IntuneWin32AppFileProcessing -Stage "CommitFile" -Resource $FilesUri + + # Update committedContentVersion property for Win32 app + Write-Verbose -Message "Updating committedContentVersion property with ID '$($Win32MobileAppContentVersionRequest.id)' for Win32 app with ID: $($Win32MobileAppRequest.id)" + $Win32AppFileCommitBody = [ordered]@{ + "@odata.type" = "#microsoft.graph.win32LobApp" + "committedContentVersion" = $Win32MobileAppContentVersionRequest.id + } + $Win32AppFileCommitBodyRequest = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps/$($Win32MobileAppRequest.id)" -Method "PATCH" -Body ($Win32AppFileCommitBody | ConvertTo-Json) + + # Handle return output + Write-Verbose -Message "Successfully created Win32 app and committed file content to Azure Storage blob" + $Win32MobileAppRequest = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "mobileApps/$($Win32MobileAppRequest.id)" -Method "GET" + Write-Output -InputObject $Win32MobileAppRequest + } + } + } + } + } + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while creating the Win32 application. 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 + } + + return $Invocation.ExitCode +} + +function Start-DownloadFile { + <# + .SYNOPSIS + Download a file from a given URL and save it in a specific location. + + .DESCRIPTION + Download a file from a given URL and save it in a specific location. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + 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 { + # Create path if it doesn't exist + if (-not(Test-Path -Path $Path)) { + New-Item -Path $Path -ItemType Directory -Force | Out-Null + } + + # Register events for tracking download progress + $Global:DownloadComplete = $false + $EventDataComplete = Register-ObjectEvent $WebClient DownloadFileCompleted -SourceIdentifier WebClient.DownloadFileComplete -Action {$Global:DownloadComplete = $true} + $EventDataProgress = Register-ObjectEvent $WebClient DownloadProgressChanged -SourceIdentifier WebClient.DownloadProgressChanged -Action { $Global:DPCEventArgs = $EventArgs } + + # Start download of file + $WebClient.DownloadFileAsync($URL, (Join-Path -Path $Path -ChildPath $Name)) + + # Track the download progress + do { + $PercentComplete = $Global:DPCEventArgs.ProgressPercentage + $DownloadedBytes = $Global:DPCEventArgs.BytesReceived + if ($DownloadedBytes -ne $null) { + Write-Progress -Activity "Downloading file: $($Name)" -Id 1 -Status "Downloaded bytes: $($DownloadedBytes)" -PercentComplete $PercentComplete + } + } + until ($Global:DownloadComplete) + } + End { + # Dispose of the WebClient object + $WebClient.Dispose() + + # Unregister events used for tracking download progress + Unregister-Event -SourceIdentifier WebClient.DownloadProgressChanged + Unregister-Event -SourceIdentifier WebClient.DownloadFileComplete + } + +} + +function Invoke-AzureStorageBlobUpload { + <# + .SYNOPSIS + Upload and commit .intunewin file into Azure Storage blob container. + + .DESCRIPTION + Upload and commit .intunewin file into Azure Storage blob container. + + This is a modified function that was originally developed by Dave Falkus and is available here: + https://github.com/microsoftgraph/powershell-intune-samples/blob/master/LOB_Application/Win32_Application_Add.ps1 + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$StorageUri, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$FilePath, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Resource + ) + $ChunkSizeInBytes = 1024l * 1024l * 6l; + + # Start the timer for SAS URI renewal + $SASRenewalTimer = [System.Diagnostics.Stopwatch]::StartNew() + + # Find the file size and open the file + $FileSize = (Get-Item -Path $FilePath).Length + $ChunkCount = [System.Math]::Ceiling($FileSize / $ChunkSizeInBytes) + $BinaryReader = New-Object -TypeName System.IO.BinaryReader([System.IO.File]::Open($FilePath, [System.IO.FileMode]::Open)) + $Position = $BinaryReader.BaseStream.Seek(0, [System.IO.SeekOrigin]::Begin) + + # Upload each chunk. Check whether a SAS URI renewal is required after each chunk is uploaded and renew if needed + $ChunkIDs = @() + for ($Chunk = 0; $Chunk -lt $ChunkCount; $Chunk++) { + $ChunkID = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($Chunk.ToString("0000"))) + $ChunkIDs += $ChunkID + $Start = $Chunk * $ChunkSizeInBytes + $Length = [System.Math]::Min($ChunkSizeInBytes, $FileSize - $Start) + $Bytes = $BinaryReader.ReadBytes($Length) + $CurrentChunk = $Chunk + 1 + + Write-Progress -Activity "Uploading File to Azure Storage blob" -Status "Uploading chunk $CurrentChunk of $ChunkCount" -PercentComplete ($CurrentChunk / $ChunkCount * 100) + $UploadResponse = Invoke-AzureStorageBlobUploadChunk -StorageUri $StorageUri -ChunkID $ChunkID -Bytes $Bytes + if (($CurrentChunk -lt $ChunkCount) -and ($SASRenewalTimer.ElapsedMilliseconds -ge 450000)) { + Invoke-AzureStorageBlobUploadRenew -Resource $Resource + $SASRenewalTimer.Restart() + } + } + + # Complete write status progress bar + Write-Progress -Completed -Activity "Uploading File to Azure Storage blob" + + # Finalize the upload of the content file to Azure Storage blob + Invoke-AzureStorageBlobUploadFinalize -StorageUri $StorageUri -ChunkID $ChunkIDs + + # Close and dispose binary reader object + $BinaryReader.Close() + $BinaryReader.Dispose() +} + +function Invoke-AzureStorageBlobUploadFinalize { + <# + .SYNOPSIS + Finalize upload of chunks of the .intunewin file into Azure Storage blob container. + + .DESCRIPTION + Finalize upload of chunks of the .intunewin file into Azure Storage blob container. + + This is a modified function that was originally developed by Dave Falkus and is available here: + https://github.com/microsoftgraph/powershell-intune-samples/blob/master/LOB_Application/Win32_Application_Add.ps1 + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$StorageUri, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Object]$ChunkID + ) + $Uri = "$($StorageUri)&comp=blocklist" + $Request = "PUT $($Uri)" + $XML = '' + foreach ($Chunk in $ChunkID) { + $XML += "$($Chunk)" + } + $XML += '' + + try { + Invoke-RestMethod -Uri $Uri -Method "Put" -Body $XML -ErrorAction Stop + } + catch { + Write-Warning -Message "Failed to finalize Azure Storage blob upload. Error message: $($_.Exception.Message)" + } +} + +function Invoke-AzureStorageBlobUploadRenew { + <# + .SYNOPSIS + Renew the SAS URI. + + .DESCRIPTION + Renew the SAS URI. + + This is a modified function that was originally developed by Dave Falkus and is available here: + https://github.com/microsoftgraph/powershell-intune-samples/blob/master/LOB_Application/Win32_Application_Add.ps1 + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Resource + ) + $RenewSASURIRequest = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource "$($Resource)/renewUpload" -Method "POST" -Body "" + $FilesProcessingRequest = Wait-IntuneWin32AppFileProcessing -Stage "AzureStorageUriRenewal" -Resource $Resource +} + +function Invoke-AzureStorageBlobUploadChunk { + <# + .SYNOPSIS + Upload a chunk of the .intunewin file into Azure Storage blob container. + + .DESCRIPTION + Upload a chunk of the .intunewin file into Azure Storage blob container. + + This is a modified function that was originally developed by Dave Falkus and is available here: + https://github.com/microsoftgraph/powershell-intune-samples/blob/master/LOB_Application/Win32_Application_Add.ps1 + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$StorageUri, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Object]$ChunkID, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Object]$Bytes + ) + $Uri = "$($StorageUri)&comp=block&blockid=$($ChunkID)" + $Request = "PUT $($Uri)" + $ISOEncoding = [System.Text.Encoding]::GetEncoding("iso-8859-1") + $EncodedBytes = $ISOEncoding.GetString($Bytes) + $Headers = @{ + "x-ms-blob-type" = "BlockBlob" + } + + try { + $WebResponse = Invoke-WebRequest $Uri -Method "Put" -Headers $Headers -Body $EncodedBytes + } + catch { + Write-Warning -Message "Failed to upload chunk to Azure Storage blob. Error message: $($_.Exception.Message)" + } +} + +function Wait-IntuneWin32AppFileProcessing { + <# + .SYNOPSIS + Wait for contentVersions/files resource processing. + + .DESCRIPTION + Wait for contentVersions/files resource processing. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Stage, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Resource + ) + do { + $GraphRequest = Invoke-IntuneGraphRequest -APIVersion "Beta" -Resource $Resource -Method "GET" + switch ($GraphRequest.uploadState) { + "$($Stage)Pending" { + Write-Verbose -Message "Intune service request for operation '$($Stage)' is in pending state, sleeping for 10 seconds" + Start-Sleep -Seconds 10 + } + "$($Stage)Failed" { + Write-Warning -Message "Intune service request for operation '$($Stage)' failed" + return $GraphRequest + } + "$($Stage)TimedOut" { + Write-Warning -Message "Intune service request for operation '$($Stage)' timed out" + return $GraphRequest + } + } + } + until ($GraphRequest.uploadState -like "$($Stage)Success") + Write-Verbose -Message "Intune service request for operation '$($Stage)' was successful with uploadState: $($GraphRequest.uploadState)" + + return $GraphRequest +} + +function Test-IntuneGraphRequest { + <# + .SYNOPSIS + Test if a certain resource is available in Intune Graph API. + + .DESCRIPTION + Test if a certain resource is available in Intune Graph API. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [ValidateSet("Beta", "v1.0")] + [string]$APIVersion, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Resource + ) + try { + # Construct full URI + $GraphURI = "https://graph.microsoft.com/$($APIVersion)/deviceAppManagement/$($Resource)" + + # Call Graph API and get JSON response + $GraphResponse = Invoke-RestMethod -Uri $GraphURI -Headers $AuthToken -Method "GET" -ErrorAction Stop -Verbose:$false + if ($GraphResponse -ne $null) { + return $true + } + } + catch [System.Exception] { + return $false + } +} + +function Invoke-IntuneGraphRequest { + <# + .SYNOPSIS + Perform a specific call to Intune Graph API, either as GET, POST or PATCH methods. + + .DESCRIPTION + Perform a specific call to Intune Graph API, either as GET, POST or PATCH methods. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + param( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [ValidateSet("Beta", "v1.0")] + [string]$APIVersion, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Resource, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [ValidateSet("GET", "POST", "PATCH")] + [string]$Method, + + [parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [System.Object]$Body, + + [parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [ValidateSet("application/json", "image/png")] + [string]$ContentType = "application/json" + ) + try { + # Construct full URI + $GraphURI = "https://graph.microsoft.com/$($APIVersion)/deviceAppManagement/$($Resource)" + Write-Verbose -Message "$($Method) $($GraphURI)" + + # Call Graph API and get JSON response + switch ($Method) { + "GET" { + $GraphResponse = Invoke-RestMethod -Uri $GraphURI -Headers $AuthToken -Method $Method -ErrorAction Stop -Verbose:$false + } + "POST" { + $GraphResponse = Invoke-RestMethod -Uri $GraphURI -Headers $AuthToken -Method $Method -Body $Body -ContentType $ContentType -ErrorAction Stop -Verbose:$false + } + "PATCH" { + $GraphResponse = Invoke-RestMethod -Uri $GraphURI -Headers $AuthToken -Method $Method -Body $Body -ContentType $ContentType -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 $($GraphURI) failed with HTTP Status $($_.Exception.Response.StatusCode) and description: $($_.Exception.Response.StatusDescription)" + } +} + +function New-IntuneWin32AppReturnCode { + <# + .SYNOPSIS + Return a hash-table with a specified return code. + + .DESCRIPTION + Return a hash-table with a specified return code. + + .PARAMETER ReturnCode + Specify the return code value for the Win32 application body. + + .PARAMETER Type + Specify the type for the return code value for the Win32 application body. Supported values are: success, softReboot, hardReboot or retry. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [parameter(Mandatory = $true, HelpMessage = "Specify the return code value for the Win32 application body.")] + [ValidateNotNullOrEmpty()] + [int]$ReturnCode, + + [parameter(Mandatory = $true, HelpMessage = "Specify the type for the return code value for the Win32 application body. Supported values are: success, softReboot, hardReboot or retry.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("success", "softReboot", "hardReboot", "retry")] + [string]$Type + ) + $ReturnCodeTable = @{ + "returnCode" = $ReturnCode + "type" = $Type + } + + return $ReturnCodeTable +} + +function Get-IntuneWin32AppDefaultReturnCode { + <# + .SYNOPSIS + Return an array of default return codes. + + .DESCRIPTION + Return an array of default return codes. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + $ReturnCodeArray = @() + $ReturnCodeArray += @{ "returnCode" = 0; "type" = "success" } + $ReturnCodeArray += @{ "returnCode" = 1707; "type" = "success" } + $ReturnCodeArray += @{ "returnCode" = 3010; "type" = "softReboot" } + $ReturnCodeArray += @{ "returnCode" = 1641; "type" = "hardReboot" } + $ReturnCodeArray += @{ "returnCode" = 1618; "type" = "retry" } + + return $ReturnCodeArray +} + +function New-IntuneWin32AppBody { + <# + .SYNOPSIS + Retrieves meta data from the detection.xml file inside the packaged Win32 application .intunewin file. + + .DESCRIPTION + Retrieves meta data from the detection.xml file inside the packaged Win32 application .intunewin file. + + .PARAMETER FilePath + Specify an existing local path to where the win32 app .intunewin file is located. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + 1.0.1 - (2020-01-27) Added support for RequirementRule parameter input + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Define that the Win32 application body will be MSI based.")] + [switch]$MSI, + + [parameter(Mandatory = $true, ParameterSetName = "EXE", HelpMessage = "Define that the Win32 application body will be File based.")] + [switch]$EXE, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify a display name for the Win32 application body.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$DisplayName, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify a description for the Win32 application body.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$Description, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify a publisher name for the Win32 application body.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$Publisher, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Specify a developer name for the Win32 application body.")] + [parameter(Mandatory = $false, ParameterSetName = "EXE")] + [string]$Developer = [string]::Empty, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the file name (e.g. name.intunewin) for the Win32 application body.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$FileName, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the setup file name (e.g. setup.exe) for the Win32 application body.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$SetupFileName, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the installation experience for the Win32 application body.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [ValidateSet("system", "user")] + [string]$InstallExperience, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the installation experience for the Win32 application body.")] + [parameter(Mandatory = $true, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [ValidateSet("allow", "basedOnReturnCode", "suppress", "force")] + [string]$RestartBehavior, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Specify the requirement rules for the Win32 application body.")] + [parameter(Mandatory = $false, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [System.Collections.Specialized.OrderedDictionary]$RequirementRule, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Provide a Base64 encoded string as icon for the Win32 application body.")] + [parameter(Mandatory = $false, ParameterSetName = "EXE")] + [ValidateNotNullOrEmpty()] + [string]$Icon, + + [parameter(Mandatory = $true, ParameterSetName = "EXE", HelpMessage = "Specify the install command line for the Win32 application body.")] + [ValidateNotNullOrEmpty()] + [string]$InstallCommandLine, + + [parameter(Mandatory = $true, ParameterSetName = "EXE", HelpMessage = "Specify the uninstall command line for the Win32 application body.")] + [ValidateNotNullOrEmpty()] + [string]$UninstallCommandLine, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the MSI installation purpose for the Win32 application body.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("DualPurpose", "PerMachine", "PerUser")] + [string]$MSIInstallPurpose, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the MSI product code for the Win32 application body.")] + [ValidateNotNullOrEmpty()] + [string]$MSIProductCode, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the MSI product name for the Win32 application body.")] + [ValidateNotNullOrEmpty()] + [string]$MSIProductName, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the MSI product version for the Win32 application body.")] + [ValidateNotNullOrEmpty()] + [string]$MSIProductVersion, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the MSI requires reboot value for the Win32 application body.")] + [ValidateNotNullOrEmpty()] + [bool]$MSIRequiresReboot, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the MSI upgrade code for the Win32 application body.")] + [ValidateNotNullOrEmpty()] + [string]$MSIUpgradeCode + ) + # Determine values for requirement rules + if ($PSBoundParameters["RequirementRule"]) { + $ApplicableArchitectures = $RequirementRule["applicableArchitectures"] + $MinimumSupportedOperatingSystem = $RequirementRule["minimumSupportedOperatingSystem"] + } + else { + $ApplicableArchitectures = "x64,x86" + $MinimumSupportedOperatingSystem = @{ + "v10_1607" = $true + } + } + + switch ($PSCmdlet.ParameterSetName) { + "MSI" { + $Win32AppBody = [ordered]@{ + "@odata.type" = "#microsoft.graph.win32LobApp" + "applicableArchitectures" = $ApplicableArchitectures + "description" = $Description + "developer" = $Developer + "displayName" = $DisplayName + "fileName" = $FileName + "setupFilePath" = $SetupFileName + "installCommandLine" = "msiexec.exe /i `"$SetupFileName`"" + "uninstallCommandLine" = "msiexec.exe /x `"$MSIProductCode`"" + "installExperience" = @{ + "runAsAccount" = $InstallExperience + "deviceRestartBehavior" = $RestartBehavior + } + "informationUrl" = $null + "isFeatured" = $false + "minimumSupportedOperatingSystem" = $MinimumSupportedOperatingSystem + "msiInformation" = @{ + "packageType" = $MSIInstallPurpose + "productCode" = $MSIProductCode + "productName" = $MSIProductName + "productVersion" = $MSIProductVersion + "publisher" = $MSIPublisher + "requiresReboot" = $MSIRequiresReboot + "upgradeCode" = $MSIUpgradeCode + }; + "notes" = "" + "owner" = "" + "privacyInformationUrl" = $null + "publisher" = $Publisher + "runAs32bit" = $false + } + + # Add icon property if pass on command line + if ($PSBoundParameters["Icon"]) { + $Win32AppBody.Add("largeIcon", @{ + "type" = "image/png" + "value" = $Icon + }) + } + } + "EXE" { + $Win32AppBody = [ordered]@{ + "@odata.type" = "#microsoft.graph.win32LobApp" + "applicableArchitectures" = "x64,x86" + "description" = $Description + "developer" = $Developer + "displayName" = $DisplayName + "fileName" = $FileName + "setupFilePath" = $SetupFileName + "installCommandLine" = $InstallCommandLine + "uninstallCommandLine" = $UninstallCommandLine + "installExperience" = @{ + "runAsAccount" = $InstallExperience + "deviceRestartBehavior" = $RestartBehavior + } + "informationUrl" = $null + "isFeatured" = $false + "minimumSupportedOperatingSystem" = @{ + "v10_1607" = $true + } + "msiInformation" = $null + "notes" = "" + "owner" = "" + "privacyInformationUrl" = $null + "publisher" = $Publisher + "runAs32bit" = $false + } + + # Add icon property if pass on command line + if ($PSBoundParameters["Icon"]) { + $Win32AppBody.Add("largeIcon", @{ + "type" = "image/png" + "value" = $Icon + }) + } + } + } + + # Handle return value with constructed Win32 application body + return $Win32AppBody +} + +function Expand-IntuneWin32AppCompressedFile { + <# + .SYNOPSIS + Expands a named file from inside the packaged Win32 application .intunewin file to a directory named as input from FolderName parameter. + + .DESCRIPTION + Expands a named file from inside the packaged Win32 application .intunewin file to a directory named as input from FolderName parameter. + + .PARAMETER FilePath + Specify an existing local path to where the win32 app .intunewin file is located. + + .PARAMETER FileName + Specify the file name inside of the Win32 app .intunewin file to be expanded. + + .PARAMETER FolderName + Specify the name of the extraction folder. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [parameter(Mandatory = $true, HelpMessage = "Specify an existing local path to where the win32 app .intunewin file is located.")] + [ValidateNotNullOrEmpty()] + [ValidatePattern("^[A-Za-z]{1}:\\\w+\\\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"; break + } + else { + # Check if file extension is intunewin + if ([System.IO.Path]::GetExtension((Split-Path -Path $_ -Leaf)) -like ".intunewin") { + return $true + } + else { + Write-Warning -Message "$(Split-Path -Path $_ -Leaf) contains unsupported file extension. Supported extension is '.intunewin'"; break + } + } + })] + [string]$FilePath, + + [parameter(Mandatory = $true, HelpMessage = "Specify the file name inside of the Win32 app .intunewin file to be expanded.")] + [ValidateNotNullOrEmpty()] + [string]$FileName, + + [parameter(Mandatory = $true, HelpMessage = "Specify the name of the extraction folder.")] + [ValidateNotNullOrEmpty()] + [string]$FolderName + ) + Begin { + # Load System.IO.Compression assembly for managing compressed files + try { + $ClassImport = Add-Type -AssemblyName "System.IO.Compression.FileSystem" -ErrorAction Stop -Verbose:$false + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while loading System.IO.Compression.FileSystem assembly. Error message: $($_.Exception.Message)"; break + } + } + Process { + try { + # Attemp to open compressed .intunewin archive file from parameter input + $IntuneWin32AppFile = [System.IO.Compression.ZipFile]::OpenRead($FilePath) + + # Construct extraction directory in the same location of the .intunewin file + $ExtractionFolderPath = Join-Path -Path (Split-Path -Path $FilePath -Parent) -ChildPath $FolderName + if (-not(Test-Path -Path ($ExtractionFolderPath))) { + New-Item -Path $ExtractionFolderPath -ItemType Directory -Force | Out-Null + } + + # Attempt to extract named file from .intunewin file + try { + if ($IntuneWin32AppFile -ne $null) { + # Determine the detection.xml file inside zip archive + $IntuneWin32AppFile.Entries | Where-Object { $_.Name -like $FileName } | ForEach-Object { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, (Join-Path -Path $ExtractionFolderPath -ChildPath $FileName), $true) + } + $IntuneWin32AppFile.Dispose() + + # Handle return value with XML content from detection.xml + return (Join-Path -Path $ExtractionFolderPath -ChildPath $FileName) + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while extracing '$($FileName)' from '$($FilePath)' file. Error message: $($_.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to open compressed '$($FilePath)' file. Error message: $($_.Exception.Message)" + } + } +} + +function New-IntuneWin32AppIcon { + <# + .SYNOPSIS + Converts a PNG/JPG/JPEG image file available locally to a Base64 encoded string. + + .DESCRIPTION + Converts a PNG/JPG/JPEG image file available locally to a Base64 encoded string. + + .PARAMETER FilePath + Specify an existing local path to where the PNG/JPG/JPEG image file is located. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [parameter(Mandatory = $true, HelpMessage = "Specify an existing local path to where the PNG/JPG/JPEG image file is located.")] + [ValidateNotNullOrEmpty()] + [ValidatePattern("^[A-Za-z]{1}:\\\w+\\\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"; break + } + else { + # Check if file extension is PNG/JPG/JPEG + $FileExtension = [System.IO.Path]::GetExtension((Split-Path -Path $_ -Leaf)) + if (($FileExtension -like ".png") -or ($FileExtension -like ".jpg") -or ($FileExtension -like ".jpeg")) { + return $true + } + else { + Write-Warning -Message "$(Split-Path -Path $_ -Leaf) contains unsupported file extension. Supported extensions are '.png', '.jpg' and '.jpeg'"; break + } + } + })] + [string]$FilePath + ) + # Handle error action preference for non-cmdlet code + $ErrorActionPreference = "Stop" + + try { + # Encode image file as Base64 string + $EncodedBase64String = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("$($FilePath)")) + Write-Output -InputObject $EncodedBase64String + } + catch [System.Exception] { + Write-Warning -Message "Failed to encode image file to Base64 encoded string. Error message: $($_.Exception.Message)" + } +} + +function Get-IntuneWin32AppMetaData { + <# + .SYNOPSIS + Retrieves meta data from the detection.xml file inside the packaged Win32 application .intunewin file. + + .DESCRIPTION + Retrieves meta data from the detection.xml file inside the packaged Win32 application .intunewin file. + + .PARAMETER FilePath + Specify an existing local path to where the Win32 app .intunewin file is located. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [parameter(Mandatory = $true, HelpMessage = "Specify an existing local path to where the win32 app .intunewin file is located.")] + [ValidateNotNullOrEmpty()] + [ValidatePattern("^[A-Za-z]{1}:\\\w+\\\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"; break + } + else { + # Check if file extension is intunewin + if ([System.IO.Path]::GetExtension((Split-Path -Path $_ -Leaf)) -like ".intunewin") { + return $true + } + else { + Write-Warning -Message "$(Split-Path -Path $_ -Leaf) contains unsupported file extension. Supported extension is '.intunewin'"; break + } + } + })] + [string]$FilePath + ) + Begin { + # Load System.IO.Compression assembly for managing compressed files + try { + $ClassImport = Add-Type -AssemblyName "System.IO.Compression.FileSystem" -ErrorAction Stop -Verbose:$false + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while loading System.IO.Compression.FileSystem assembly. Error message: $($_.Exception.Message)"; break + } + } + Process { + try { + # Attemp to open compressed .intunewin archive file from parameter input + $IntuneWin32AppFile = [System.IO.Compression.ZipFile]::OpenRead($FilePath) + + # Attempt to extract meta data from .intunewin file + try { + if ($IntuneWin32AppFile -ne $null) { + # Determine the detection.xml file inside zip archive + $DetectionXMLFile = $IntuneWin32AppFile.Entries | Where-Object { $_.Name -like "detection.xml" } + + # Open the detection.xml file + $FileStream = $DetectionXMLFile.Open() + + # Construct new stream reader, pass file stream and read XML content to the end of the file + $StreamReader = New-Object -TypeName "System.IO.StreamReader" -ArgumentList $FileStream -ErrorAction Stop + $DetectionXMLContent = [xml]($StreamReader.ReadToEnd()) + + # Close and dispose objects to preserve memory usage + $FileStream.Close() + $StreamReader.Close() + $IntuneWin32AppFile.Dispose() + + # Handle return value with XML content from detection.xml + return $DetectionXMLContent + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while reading application information from detection.xml file. Error message: $($_.Exception.Message)" + } + } + catch [System.Exception] { + Write-Warning -Message "An error occurred while attempting to open compressed '$($FilePath)' file. Error message: $($_.Exception.Message)" + } + } +} + +function New-IntuneWin32AppRequirementRule { + <# + .SYNOPSIS + Construct a new requirement rule as an optional requirement for Add-IntuneWin32App cmdlet. + + .DESCRIPTION + Construct a new requirement rule as an optional requirement for Add-IntuneWin32App cmdlet. + + .PARAMETER Architecture + Specify the architecture as a requirement for the Win32 app. + + .PARAMETER MinimumSupportedOperatingSystem + Specify the minimum supported operating system version as a requirement for the Win32 app. + + .PARAMETER MinimumFreeDiskSpaceInMB + Specify the minimum free disk space in MB as a requirement for the Win32 app. + + .PARAMETER MinimumMemoryInMB + Specify the minimum required memory in MB as a requirement for the Win32 app. + + .PARAMETER MinimumNumberOfProcessors + Specify the minimum number of required logical processors as a requirement for the Win32 app. + + .PARAMETER MinimumCPUSpeedInMHz + Specify the minimum CPU speed in Mhz (as an integer) as a requirement for the Win32 app. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-27 + Updated: 2020-01-27 + + Version history: + 1.0.0 - (2020-01-27) Function created + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [parameter(Mandatory = $true, HelpMessage = "Specify the architecture as a requirement for the Win32 app.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("x64", "x86", "All")] + [string]$Architecture, + + [parameter(Mandatory = $true, HelpMessage = "Specify the minimum supported operating system version as a requirement for the Win32 app.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("1607", "1703", "1709", "1803", "1809", "1903")] + [string]$MinimumSupportedOperatingSystem, + + [parameter(Mandatory = $false, HelpMessage = "Specify the minimum free disk space in MB as a requirement for the Win32 app.")] + [ValidateNotNullOrEmpty()] + [int]$MinimumFreeDiskSpaceInMB, + + [parameter(Mandatory = $false, HelpMessage = "Specify the minimum required memory in MB as a requirement for the Win32 app.")] + [ValidateNotNullOrEmpty()] + [int]$MinimumMemoryInMB, + + [parameter(Mandatory = $false, HelpMessage = "Specify the minimum number of required logical processors as a requirement for the Win32 app.")] + [ValidateNotNullOrEmpty()] + [int]$MinimumNumberOfProcessors, + + [parameter(Mandatory = $false, HelpMessage = "Specify the minimum CPU speed in Mhz (as an integer) as a requirement for the Win32 app.")] + [ValidateNotNullOrEmpty()] + [int]$MinimumCPUSpeedInMHz + ) + # Construct table for supported architectures + $ArchitectureTable = @{ + "x64" = "x64" + "x86" = "x86" + "All" = "x64,x86" + } + + # Construct table for supported operating systems + $OperatingSystemTable = @{ + "1607" = "v10_1607" + "1703" = "v10_1703" + "1709" = "v10_1709" + "1803" = "v10_1803" + "1809" = "v10_1809" + "1903" = "v10_1903" + #"1909" = "v10_1909" + } + + # Construct ordered hash-table with least amount of required properties for default requirement rule + $RequirementRule = [ordered]@{ + "applicableArchitectures" = $ArchitectureTable[$Architecture] + "minimumSupportedOperatingSystem" = @{ + $OperatingSystemTable[$MinimumSupportedOperatingSystem] = $true + } + } + + # Add additional requirement rule details if specified on command line + if ($PSBoundParameters["MinimumFreeDiskSpaceInMB"]) { + $RequirementRule.Add("minimumFreeDiskSpaceInMB", $MinimumFreeDiskSpaceInMB) + } + if ($PSBoundParameters["MinimumMemoryInMB"]) { + $RequirementRule.Add("minimumMemoryInMB", $MinimumMemoryInMB) + } + if ($PSBoundParameters["MinimumNumberOfProcessors"]) { + $RequirementRule.Add("minimumNumberOfProcessors", $MinimumNumberOfProcessors) + } + if ($PSBoundParameters["MinimumCPUSpeedInMHz"]) { + $RequirementRule.Add("minimumCpuSpeedInMHz", $MinimumCPUSpeedInMHz) + } + + return $RequirementRule +} + +function New-IntuneWin32AppDetectionRule { + <# + .SYNOPSIS + Construct a new detection rule required for Add-IntuneWin32App cmdlet. + + .DESCRIPTION + Construct a new detection rule required for Add-IntuneWin32App cmdlet. + + .PARAMETER MSI + Define that the detection rule will be MSI based. + + .PARAMETER File + Define that the detection rule will be File based. + + .PARAMETER Registry + Define that the detection rule will be Registry based. + + .PARAMETER PowerShellScript + Define that the detection rule will be PowerShell script based. + + .PARAMETER MSIProductCode + Specify the MSI product code for the application. + + .PARAMETER MSIProductVersionOperator + Specify the MSI product version operator. Supported values are: notConfigured, equal, notEqual, greaterThanOrEqual, greaterThan, lessThanOrEqual or lessThan. + + .PARAMETER MSIProductVersion + Specify the MSI product version, e.g. 1.0.0. + + .PARAMETER FilePath + Specify the path for a folder or file. + + .PARAMETER FileOrFolderName + Specify the folder or file name. + + .PARAMETER FileDetectionType + Specify the file detection type. Supported values are: notConfigured, exists, modifiedDate, createdDate, version or sizeInMB. + + .PARAMETER FileDetectionValue + Specify the file detection value. + + .PARAMETER Check32BitOn64System + Specify if detection should check for 32-bit on 64-bit systems. + + .PARAMETER RegistryKeyPath + Specify the registry key path, e.g. 'HKEY_LOCAL_MACHINE\SOFTWARE\Program'. + + .PARAMETER RegistryDetectionType + Specify the registry detection type. Supported values are: exists, doesNotExist, string, integer or version. + + .PARAMETER RegistryValueName + Specify the registry value name. + + .PARAMETER Check32BitRegOn64System + Specify if detection should check for 32-bit on 64-bit system. + + .PARAMETER ScriptFile + Specify the full path to the PowerShell detection script, e.g. 'C:\Scripts\Detection.ps1'. + + .PARAMETER EnforceSignatureCheck + Specify if PowerShell script signature check should be enforced. + + .PARAMETER RunAs32Bit + Specify is PowerShell script should be executed as a 32-bit process. + + .NOTES + Author: Nickolaj Andersen + Contact: @NickolajA + Created: 2020-01-04 + Updated: 2020-01-04 + + Version history: + 1.0.0 - (2020-01-04) Function created + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Define that the detection rule will be MSI based.")] + [switch]$MSI, + + [parameter(Mandatory = $true, ParameterSetName = "File", HelpMessage = "Define that the detection rule will be File based.")] + [switch]$File, + + [parameter(Mandatory = $true, ParameterSetName = "Registry", HelpMessage = "Define that the detection rule will be Registry based.")] + [switch]$Registry, + + [parameter(Mandatory = $true, ParameterSetName = "PowerShell", HelpMessage = "Define that the detection rule will be PowerShell script based.")] + [switch]$PowerShellScript, + + [parameter(Mandatory = $true, ParameterSetName = "MSI", HelpMessage = "Specify the MSI product code for the application.")] + [ValidateNotNullOrEmpty()] + [string]$MSIProductCode, + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Specify the MSI product version operator. Supported values are: notConfigured, equal, notEqual, greaterThanOrEqual, greaterThan, lessThanOrEqual or lessThan.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("notConfigured", "equal", "notEqual", "greaterThanOrEqual", "greaterThan", "lessThanOrEqual", "lessThan")] + [string]$MSIProductVersionOperator = "notConfigured", + + [parameter(Mandatory = $false, ParameterSetName = "MSI", HelpMessage = "Specify the MSI product version, e.g. 1.0.0.")] + [ValidateNotNullOrEmpty()] + [string]$MSIProductVersion = [string]::Empty, + + [parameter(Mandatory = $true, ParameterSetName = "File", HelpMessage = "Specify the path for a folder or file.")] + [ValidateNotNullOrEmpty()] + [string]$FilePath, + + [parameter(Mandatory = $true, ParameterSetName = "File", HelpMessage = "Specify the folder or file name.")] + [ValidateNotNullOrEmpty()] + [string]$FileOrFolderName, + + [parameter(Mandatory = $false, ParameterSetName = "File", HelpMessage = "Specify the file detection type. Supported values are: notConfigured, exists, modifiedDate, createdDate, version or sizeInMB.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("notConfigured", "exists", "modifiedDate", "createdDate", "version", "sizeInMB")] + [string]$FileDetectionType = "notConfigured", + + [parameter(Mandatory = $false, ParameterSetName = "File", HelpMessage = "Specify the file detection value.")] + [ValidateNotNullOrEmpty()] + [string]$FileDetectionValue = [string]::Empty, + + [parameter(Mandatory = $false, ParameterSetName = "File", HelpMessage = "Specify if detection should check for 32-bit on 64-bit systems.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("True", "False")] + [string]$Check32BitOn64System = "False", + + [parameter(Mandatory = $true, ParameterSetName = "Registry", HelpMessage = "Specify the registry key path, e.g. 'HKEY_LOCAL_MACHINE\SOFTWARE\Program'.")] + [ValidateNotNullOrEmpty()] + [string]$RegistryKeyPath, + + [parameter(Mandatory = $true, ParameterSetName = "Registry", HelpMessage = "Specify the registry detection type. Supported values are: exists, doesNotExist, string, integer or version.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("exists", "doesNotExist", "string", "integer", "version")] + [string]$RegistryDetectionType, + + [parameter(Mandatory = $false, ParameterSetName = "Registry", HelpMessage = "Specify the registry value name.")] + [ValidateNotNullOrEmpty()] + [string]$RegistryValueName, + + [parameter(Mandatory = $false, ParameterSetName = "Registry", HelpMessage = "Specify if detection should check for 32-bit on 64-bit system.")] + [ValidateNotNullOrEmpty()] + [ValidateSet("True","False")] + [string]$Check32BitRegOn64System = "False", + + [parameter(Mandatory = $true, ParameterSetName = "PowerShell", HelpMessage = "Specify the full path to the PowerShell detection script, e.g. 'C:\Scripts\Detection.ps1'.")] + [ValidateNotNullOrEmpty()] + [string]$ScriptFile, + + [parameter(Mandatory = $false, ParameterSetName = "PowerShell", HelpMessage = "Specify if PowerShell script signature check should be enforced.")] + [ValidateNotNullOrEmpty()] + [bool]$EnforceSignatureCheck = $false, + + [parameter(Mandatory = $false, ParameterSetName = "PowerShell", HelpMessage = "Specify is PowerShell script should be executed as a 32-bit process.")] + [ValidateNotNullOrEmpty()] + [bool]$RunAs32Bit = $false + ) + # Handle initial value for return + $DetectionRule = $null + + # Determine detection rule generation method based upon parameter set name + switch ($PSCmdlet.ParameterSetName) { + "MSI" { + $DetectionRule = [ordered]@{ + "@odata.type" = "#microsoft.graph.win32LobAppProductCodeDetection" + "productCode" = $MSIProductCode + "productVersionOperator" = $MSIProductVersionOperator + "productVersion" = $MSIProductVersion + } + } + "File" { + # NOTE: Currently only supports detection method type as "File or folder exists", other methods will be implemented in a future release + $DetectionRule = [ordered]@{ + "@odata.type" = "#microsoft.graph.win32LobAppFileSystemDetection" + "check32BitOn64System" = $Check32BitOn64System + "detectionType" = $FileDetectionType + "detectionValue" = $FileDetectionValue + "fileOrFolderName" = $FileOrFolderName + "operator" = "notConfigured" + "path" = $FilePath + } + } + "Registry" { + # NOTE: Currently only supports detection method type as "Key/Value exists", other methods will be implemented in a future release + $DetectionRule = [ordered]@{ + "@odata.type" = "#microsoft.graph.win32LobAppRegistryDetection" + "check32BitOn64System" = $Check32BitRegOn64System + "detectionType" = "exists" + "detectionValue" = "" + "keyPath" = $RegistryKeyPath + "operator" = "notConfigured" + } + + # Handle valueName property value depending on parameter input + if ($PSBoundParameters["RegistryValueName"]) { + $DetectionRule.Add("valueName", $RegistryValueName) + } + else { + $DetectionRule.Add("valueName", [string]::Empty) + } + } + "PowerShell" { + # Detect if passed script file exists + if (Test-Path -Path $ScriptFile) { + # Convert script file contents to base64 string + $ScriptContent = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("$($ScriptFile)")) + + # Construct detection rule ordered table + $DetectionRule = [ordered]@{ + "@odata.type" = "#microsoft.graph.win32LobAppPowerShellScriptDetection" + "enforceSignatureCheck" = $EnforceSignatureCheck + "runAs32Bit" = $RunAs32Bit + "scriptContent" = $ScriptContent + } + } + else { + Write-Warning -Message "Unable to detect the presence of specified script file" + } + } + } + + # Handle return value with constructed detection rule + return $DetectionRule +} \ No newline at end of file diff --git a/Modules/IntuneWin32App/README.md b/Modules/IntuneWin32App/README.md new file mode 100644 index 0000000..c500492 --- /dev/null +++ b/Modules/IntuneWin32App/README.md @@ -0,0 +1,179 @@ +# Overview +This module was created to provide means to automate the packaging, creation and publishing of Win32 applications in Microsoft Intune. + +Currently the following functions are supported in the module: +- Get-IntuneWin32App +- Get-IntuneWin32AppMetaData +- Add-IntuneWin32App +- Add-IntuneWin32AppAssignment +- New-IntuneWin32AppPackage +- New-IntuneWin32AppDetectionRule +- New-IntuneWin32AppReturnCode +- New-IntuneWin32AppIcon +- Expand-IntuneWin32AppPackage + +## Installing the module from PSGallery +The IntuneWin32App module is published to the PowerShell Gallery. Install it on your system by running the following in an elevated PowerShell console: +```PowerShell +Install-Module -Name "IntuneWin32App" +``` + +## Module and authentication requirements +IntuneWin32App module requires the following modules to be installed on the system where it's used: +- AzureAD +- PSIntuneAuth + +Delegated authentication (username / password) is currently the only authentication mechanism that's being supported. App-based authentication will be added in a future release. + +## Common parameter inputs +A set of functions in this module, those that interact with Microsoft Intune (essentially query the Graph API for resources), all have common parameters that requires input. These parameters are: +- TenantName + - This parameter should be given the full tenant name, e.g. name.onmicrosoft.com. +- ApplicationID (optional) + - Provide the Application ID of the app registration in Azure AD. By default, the script will attempt to use the well known Microsoft Intune PowerShell app registration. +- PromptBehavior (optional) + - Define the prompt behavior when acquiring a token. Possible values are: Auto, Always, Never, RefreshSession + +The functions that have these parameters, an authorization token is acquired. This will by default happen for the sign-in user, if possible. For scenarios when another credential is required to acquire the authorization token, specify Always as the value for PromptBehavior. + +## Get existing Win32 apps +Get-IntuneWin32App function can be used to retrieve existing Win32 apps in Microsoft Intune. Retrieving an existing Win32 app could either be done passing the display name of the app, which performs a wildcard search meaning it's not required to specify the full name of the Win32 app. The ID if a specific Win32 app could also be used for this function. Additionally, by not specifying either a display name or an ID, all Win32 apps available will be retrieved. Below are a few examples of how this function could be used: +```PowerShell +# Get all Win32 apps +Get-IntuneWin32App -TenantName "name.onmicrosoft.com" -Verbose + +# Get a specific Win32 app by it's display name +Get-IntuneWin32App -TenantName "name.onmicrosoft.com" -DisplayName "7-zip" -Verbose + +# Get a specific Win32 app by it's id +Get-IntuneWin32App -TenantName "name.onmicrosoft.com" -ID "" -Verbose +``` + +## Package application source files into Win32 app package (.intunewin) +Use the New-IntuneWin32AppPackage function in the module to create a content package for a Win32 app. MSI, EXE and script-based applications are supported by this function. This function automatically downloads the IntuneWinAppUtil.exe application that's essentially the engine behind the packaging and encryption process. The utility will be downloaded to the temporary directory of the user running the function, more specifically the location of the environment variable %TEMP%. If required, a custom path to where IntuneWinAppUtil.exe already exists is possible to pass to the function using the IntuneWinAppUtilPath parameter. In the sample below, application source files for 7-Zip including the setup file are specified and being packaged into an .intunewin encrypted file. Package will be exported to the output folder. +```PowerShell +# Package MSI as .intunewin file +$SourceFolder = "C:\IntuneWinAppUtil\Source\7-Zip" +$SetupFile = "7z1900-x64.msi" +$OutputFolder = "C:\IntuneWinAppUtil\Output" +New-IntuneWin32AppPackage -SourceFolder $SourceFolder -SetupFile $SetupFile -OutputFolder $OutputFolder -Verbose +``` + +## Create a new MSI based installation as a Win32 app +Use the New-IntuneWin32AppPackage function to first create the packaged Win32 app content file (.intunewin). Then call the Add-IntuneWin32App function to create a new Win32 app in Microsoft Intune. This function has dependencies for other functions in the module. For instance when passing the detection rule for the Win32 app, you need to use the New-IntuneWin32AppDetectionRule function to create the required input object. Below is an example how the dependent functions in this module can be used together with the Add-IntuneWin32App function to successfully upload a packaged Win32 app content file to Microsoft Intune: +```PowerShell +# Get MSI meta data from .intunewin file +$IntuneWinFile = "C:\IntuneWinAppUtil\Output\7z1900-x64.intunewin" +$IntuneWinMetaData = Get-IntuneWin32AppMetaData -FilePath $IntuneWinFile + +# Create custom display name like 'Name' and 'Version' +$DisplayName = $IntuneWinMetaData.ApplicationInfo.Name + " " + $IntuneWinMetaData.ApplicationInfo.MsiInfo.MsiProductVersion + +# Create MSI detection rule +$DetectionRule = New-IntuneWin32AppDetectionRule -MSI -MSIProductCode $IntuneWinMetaData.ApplicationInfo.MsiInfo.MsiProductCode + +# Add new MSI Win32 app +Add-IntuneWin32App -TenantName "name.onmicrosoft.com" -FilePath $IntuneWinFile -DisplayName $DisplayName -InstallExperience "system" -RestartBehavior "suppress" -DetectionRule $DetectionRule -Verbose +``` + +## Create a new EXE/script based installation as a Win32 app +Use the New-IntuneWin32AppPackage function to first create the packaged Win32 app content file (.intunewin). Then call the Add-IntuneWin32App much like the example above illustrates for a MSI installation based Win32 app. Apart from the above example, for an EXE/script based Win32 app, a few other parameters are required: +- InstallCommandLine +- UninstallCommandLine + +The detection rule is also constructed differently, for example in the below script it's using a PowerShell script as the detection logic. In the example below a Win32 app is created that's essentially a PowerShell script that executes and another PowerShell script used for detection: +```PowerShell +# Get MSI meta data from .intunewin file +$IntuneWinFile = "C:\IntuneWinAppUtil\Output\Enable-BitLockerEncryption.intunewin" +$IntuneWinMetaData = Get-IntuneWin32AppMetaData -FilePath $IntuneWinFile + +# Create custom display name like 'Name' and 'Version' +$DisplayName = "Enable BitLocker Encryption 1.0" + +# Create PowerShell script detection rule +$DetectionScriptFile = "C:\IntuneWinAppUtil\Output\Get-BitLockerEncryptionDetection.ps1" +$DetectionRule = New-IntuneWin32AppDetectionRule -PowerShellScript -ScriptFile $DetectionScriptFile -EnforceSignatureCheck $false -RunAs32Bit $false + +# Add new EXE Win32 app +$InstallCommandLine = "powershell.exe -ExecutionPolicy Bypass -File .\Enable-BitLockerEncryption.ps1" +$UninstallCommandLine = "cmd.exe /c" +Add-IntuneWin32App -TenantName "name.onmicrosoft.com" -FilePath $IntuneWinFile -DisplayName $DisplayName -Description "Start BitLocker silent encryption" -Publisher "SCConfigMgr" -InstallExperience "system" -RestartBehavior "suppress" -DetectionRule $DetectionRule -ReturnCode $ReturnCode -InstallCommandLine $InstallCommandLine -UninstallCommandLine $UninstallCommandLine -Verbose +``` + +## Additional parameters for Add-IntuneWin32App function +When creating a Win32 app, additional configuration is possible when using the Add-IntuneWin32App function. It's possible to set the icon for the Win32 app using the Icon parameter. If desired, it's also possible to add custom, in addition to the default, return codes by adding the ReturnCode parameter. Below is an example of how the Add-IntuneWin32App function could be extended with those parameters by using the New-IntuneWin32AppIcon and New-IntuneWin32AppReturnCode functions: + +```PowerShell +# Create custom return code +$ReturnCode = New-IntuneWin32AppReturnCode -ReturnCode 1337 -Type "retry" + +# Convert image file to icon +$ImageFile = "C:\IntuneWinAppUtil\Logos\Image.png" +$Icon = New-IntuneWin32AppIcon -FilePath $ImageFile +``` + +## Create a Win32 app assignment +IntuneWin32App module also supports adding assignments. In version 1.0.0, functionality for creating an assignment for an existing Win32 app in Microsoft Intune (or one created with the Add-IntuneWin32App function), consists of targeting for: +- All Users +- Specified group + +Assignments created with this module doesn't currently support specifying an installation deadline or available time. The assignment will by default be created with the settings for installation deadline and availability configured as 'As soon as possible'. Below is an example of how to add assignments using the module: +### Adding for a group +```PowerShell +# Get a specific Win32 app by it's display name +$Win32App = Get-IntuneWin32App -TenantName "name.onmicrosoft.com" -DisplayName "7-zip" -Verbose + +# Add assignment for a specific Azure AD group +$GroupID = "" +Add-IntuneWin32AppAssignment -TenantName "name.onmicrosoft.com" -DisplayName $Win32App.displayName -Target "Group" -GroupID $GroupID -Intent "available" -Notification "showAll" -Verbose +``` +### Adding for all users +```PowerShell +# Get a specific Win32 app by it's display name +$Win32App = Get-IntuneWin32App -TenantName "name.onmicrosoft.com" -DisplayName "7-zip" -Verbose + +# Add assignment for all users +Add-IntuneWin32AppAssignment -TenantName "name.onmicrosoft.com" -DisplayName $Win32App.displayName -Target "AllUsers" -Intent "available" -Notification "showAll" -Verbose +``` + +## Expand +The New-IntuneWin32AppPackage function packages and encrypts a Win32 app content file (.intunewin file). This file can be uncompressed using any decompression tool, e.g. 7-Zip. Inside the file resides a folder structure resides essentially two important files for that's required for the Expand-IntuneWin32AppPackage function. These two files, detection.xml .intunewin, was generated when IntuneWinAppUtil.exe executed. Detection.xml contains the encryption info, more specifically the encryptionKey and initializationVector details. .intunewin is the actual encrypted file, that with the encryptionKey and initializationVector info, can be decrypted. This function can 'expand', meaning to uncompress and decrypt the original Win32 app content file containing the two files already mentioned, but does not support decryption only of the .intunewin file that was already uploaded to Microsoft Intune for a given Win32 app and then later downloaded from the Azure Storage blob associated with that app. This is because Graph API does not expose the encryptionKey and initializationVector data once a Win32 app content file has been uploaded to Microsoft Intune. A request to expose this data in Graph API has been sent to Microsoft, but the future will tell if they decide to fullfil that request. Below is an example of how to use the Expand-IntuneWin32AppPackage function using the full Win32 app content file created either manually with IntuneWinAppUtil.exe or with the New-IntuneWin32AppPackage function: + +```PowerShell +# Decode an existing Win32 app content file +$IntuneWinFile = "C:\IntuneWinAppUtil\Output\7z1900-x64.intunewin" +Expand-IntuneWin32AppPackage -FilePath $IntuneWinFile -Force -Verbose +``` + +## Full example of packaging and creating a Win32 app +Below is an example that automates the complete process of creating the Win32 app content file, adding a new Win32 app in Microsoft Intune and assigns it to all users. +```PowerShell +# Package MSI as .intunewin file +$SourceFolder = "C:\IntuneWinAppUtil\Source\7-Zip" +$SetupFile = "7z1900-x64.msi" +$OutputFolder = "C:\IntuneWinAppUtil\Output" +$Win32AppPackage = New-IntuneWin32AppPackage -SourceFolder $SourceFolder -SetupFile $SetupFile -OutputFolder $OutputFolder -Verbose + +# Get MSI meta data from .intunewin file +$IntuneWinFile = $Win32AppPackage.Path +$IntuneWinMetaData = Get-IntuneWin32AppMetaData -FilePath $IntuneWinFile + +# Create custom display name like 'Name' and 'Version' +$DisplayName = $IntuneWinMetaData.ApplicationInfo.Name + " " + $IntuneWinMetaData.ApplicationInfo.MsiInfo.MsiProductVersion + +# Create MSI detection rule +$DetectionRule = New-IntuneWin32AppDetectionRule -MSI -MSIProductCode $IntuneWinMetaData.ApplicationInfo.MsiInfo.MsiProductCode + +# Create custom return code +$ReturnCode = New-IntuneWin32AppReturnCode -ReturnCode 1337 -Type "retry" + +# Convert image file to icon +$ImageFile = "C:\IntuneWinAppUtil\Logos\7-Zip.png" +$Icon = New-IntuneWin32AppIcon -FilePath $ImageFile + +# Add new MSI Win32 app +$Win32App = Add-IntuneWin32App -TenantName "name.onmicrosoft.com" -FilePath $IntuneWinFile -DisplayName $DisplayName -InstallExperience "system" -RestartBehavior "suppress" -DetectionRule $DetectionRule -ReturnCode $ReturnCode -Icon $Icon -Verbose + +# Add assignment for all users +Add-IntuneWin32AppAssignment -TenantName "name.onmicrosoft.com" -DisplayName $Win32App.displayName -Target "AllUsers" -Intent "available" -Notification "showAll" -Verbose +``` \ No newline at end of file diff --git a/Modules/IntuneWin32App/ReleaseNotes.md b/Modules/IntuneWin32App/ReleaseNotes.md new file mode 100644 index 0000000..aa794a0 --- /dev/null +++ b/Modules/IntuneWin32App/ReleaseNotes.md @@ -0,0 +1,12 @@ +# Release notes for IntuneWin32App module + +## 1.1.0 +- Added a new function called Get-MSIMetaData to retrieve MSI file properties like ProductCode and more. +- Added a new function called New-IntuneWin32AppRequirementRule to create a customized requirement rule for the Win32 app. This function does not support 'Additional requirement rules' as of yet, but will be implemented in a future version. +- Function Add-IntuneWin32App now supports an optional RequirementRule parameter. Use New-IntuneWin32AppRequirementRule function to create a suitable object for this parameter. If the RequirementRule parameter is not specified for Add-IntuneWin32App, default values of 'applicableArchitectures' with a value of 'x64,x86' and 'minimumSupportedOperatingSystem' with a value of 'v10_1607' will be used when adding the Win32 app. + +## 1.0.1 +- Updated Get-IntuneWin32App function to load all properties for objects return and support multiple objects returned for wildcard search when specifying display name + +## 1.0.0 +- Initial release, se README.md for documentation. \ No newline at end of file diff --git a/Modules/PSIntuneAuth/PSIntuneAuth.psd1 b/Modules/PSIntuneAuth/PSIntuneAuth.psd1 index 8c9ea04..58a3ba6 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.2' # 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 = @() diff --git a/Modules/PSIntuneAuth/PSIntuneAuth.psm1 b/Modules/PSIntuneAuth/PSIntuneAuth.psm1 index 3545c7a..17d3290 100644 --- a/Modules/PSIntuneAuth/PSIntuneAuth.psm1 +++ b/Modules/PSIntuneAuth/PSIntuneAuth.psm1 @@ -8,24 +8,272 @@ 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: 2020-01-28 + + 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 + #> + [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 -ErrorAction Stop -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 + Install-Module -Name "AzureAD" -Scope AllUsers -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 + } + } + + 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 +282,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 +309,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 +353,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..ed22c7c --- /dev/null +++ b/Modules/README.md @@ -0,0 +1,9 @@ +# PSIntuneAuth +Provides a function to retrieve an authentication token for Intune Graph API calls. + +![PowerShell Gallery](https://img.shields.io/powershellgallery/dt/PSIntuneAuth) + +# IntuneWin32App +Provides a set of functions to package and add a Win32 app to Microsoft Endpoint Manager (Intune). + +![PowerShell Gallery](https://img.shields.io/powershellgallery/dt/IntuneWin32App) \ No newline at end of file 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/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: