diff --git a/CollectSCCMDeviceLogsAutomation/CollectDeviceLogs/Collect Logs to Cloud.ps1 b/CollectSCCMDeviceLogsAutomation/CollectDeviceLogs/Collect Logs to Cloud.ps1 new file mode 100644 index 0000000..b293449 --- /dev/null +++ b/CollectSCCMDeviceLogsAutomation/CollectDeviceLogs/Collect Logs to Cloud.ps1 @@ -0,0 +1,39 @@ +param +( +    [Parameter(Mandatory=$true)] +    $EscapedSASToken, +    [Parameter(Mandatory=$true)] +    $StorageURL, +    [Parameter(Mandatory=$true)] +    $FilesOrFolders +)  +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +$SASToken =  [uri]::UnescapeDataString($EscapedSASToken)  + +$FilesOrFoldersColletion =  $FilesOrFolders.Split(',', [StringSplitOptions]::RemoveEmptyEntries) +foreach ($FileOrFolder in $FilesOrFoldersColletion) +{ +    if ((Test-Path -Path $FileOrFolder) -eq $false) +    { +        Write-Warning "'$FileOrFolders' does not exist" +        continue +    } +    $Headers = @{ +        'x-ms-blob-type' = 'BlockBlob' +    } +    $FilesToUpload = Get-ChildItem -Path $FileOrFolder -Recurse |  Where-Object { ! $_.PSIsContainer } | %{$_.FullName} +    foreach ($File in $FilesToUpload) +    { +        $Uri = "{0}/$env:COMPUTERNAME/{1}{2}" -f $StorageURL, $File , $SASToken +        try  +        { +           Invoke-RestMethod  -Uri $Uri -Method Put -Headers $Headers -InFile $File | Out-Null +        } +        catch  +        { +            #file is being used by another process. +        } +    } +    Write-Output "File(s) Uploaded" +} \ No newline at end of file diff --git a/CollectSCCMDeviceLogsAutomation/CollectDeviceLogs/Collect-DeviceLogs.ps1 b/CollectSCCMDeviceLogsAutomation/CollectDeviceLogs/Collect-DeviceLogs.ps1 new file mode 100644 index 0000000..5c8a019 --- /dev/null +++ b/CollectSCCMDeviceLogsAutomation/CollectDeviceLogs/Collect-DeviceLogs.ps1 @@ -0,0 +1,295 @@ +<# +.SYNOPSIS + Colect Device Logs from a Collection ID or Device Name in SCCM +.DESCRIPTION + Powershell script used to collect device logs from user devices from SCCM. + It works by connecting to SCCM, running a Powershell script called Collect Logs to Cloud + stored in SCCM on each device collecting specific logs, connecting to an Azure Storage + Account with an MSI using Client Id, and then sending the logs to the Storage Account. An + HTML Report is also generated in the local filepath with the results. +.PARAMETER SiteCode + SCCM Site Code +.PARAMETER SiteServer + SCCM Site Server +.PARAMETER StorageAccountName + Name of Azure Storage Account +.PARAMETER StorageAccountResourceGroup + Name of Azure Storage Account Resource Group +.PARAMETER Container + Name of Container in Azure Storage Account +.PARAMETER ClientId + Azure MSI Client Id used to get the Storage Account Access Token +.NOTES + Version: 1.0 + Creation date: 02/09/2023 + Purpose/Change: Open Source example +#> + +$ErrorActionPreference = "Stop" +$SiteCode = "" #Enter SCCM Site Code +$SiteServer = "" #Enter SCCM Site Server Fully Qualified Domain Name +$Namespace = "root\sms\site_$SiteCode" +$StorageAccountName = "" #Enter Azure Storage Account Name where logs will be sent +$StorageAccountResourceGroup = "" #Enter Azure Storage Account Resource Group Name where logs will be sent +$Container = "" #Enter Azure Storage Account Container Name where logs will be sent +$StorageURL = "https://$($StorageAccountName).blob.core.windows.net/$($Container)" +enum EnumExecutionState +{ + Succeeded = 1 + Failed = 2 +} +function Create-HTMLTable +{ + param( + [Parameter(Mandatory)] + $TableData + ) + $CSS = "" + + return ($TableData | ConvertTo-HTML -as Table -Fragment -PreContent $CSS | Out-String) +} +function Connect-ConfigMgrProvider($SiteCode, $SiteServer) +{ + $initParams = @{} + $Module = (Get-Item $env:SMS_ADMIN_UI_PATH).Parent.FullName + "\ConfigurationManager.psd1" + Import-Module $Module @initParams + + if ((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) { + New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $SiteServer -Scope Script @initParams | Out-Null + } + Set-Location "$($SiteCode):\" @initParams | Out-Null +} +function Get-MsiAccessToken +{ + param + ( + [Parameter(Mandatory=$true)] + [Validateset("https://graph.microsoft.com","https://vault.azure.net","https://management.azure.com","https://database.windows.net")] + $ResourceUri, + [Parameter(Mandatory=$true)] + $ClientId + ) + $msiEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=$($ResourceUri)&client_id=$($ClientId)" + $response = Invoke-WebRequest -Uri $msiEndpoint -Headers @{"Metadata"= $true} -UseBasicParsing + $responseJson = ConvertFrom-Json -InputObject $response + $AccessToken = $responseJson.access_token + return $AccessToken +} +function Get-SASToken($StorageAccountName, $StorageAccountResourceGroup) +{ + $ClientId = "" #Enter MSI Client Id with access to the Azure Storage Account + $AccessToken = Get-MsiAccessToken -ResourceUri https://management.azure.com -ClientId $ClientId + Connect-AzAccount -AccessToken $AccessToken -AccountId $ManagedIdentity | Out-Null + $StorageAccountAccessKey = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccountResourceGroup -Name $StorageAccountName).Value[0] + $StorageContext = New-AzStorageContext $StorageAccountName -StorageAccountKey $StorageAccountAccessKey + New-AzStorageAccountSASToken -Context $StorageContext -Service Blob -ResourceType Object -Permission "rw" -ExpiryTime (Get-Date).AddHours(1) +} +function WaitToComplete($OperationId) +{ + Write-Output "OperationId - $($OperationId) - checking Runscript execution status" + #Get script output, wait a maximum of 30 minutes + $pollTimeOut = (Get-Date).AddMinutes(30) + do + { + $ExecutionStatuses = Get-CimInstance -ComputerName $SiteServer -Namespace $Namespace -ClassName SMS_ScriptsExecutionStatus -Filter "ClientOperationID = '$OperationId'" + if ($ExecutionStatuses) + { + Write-Output "RunScript Execution Completed : OperationId - $($OperationId)" + return $ExecutionStatuses + } + + Write-Output "OperationId - $($OperationId) - Sleeping for 15 seconds for check Runscript execution status" + Start-Sleep -Seconds 15 + } + while ((Get-date) -le $pollTimeOut) + Write-Output "OperationId - $($OperationId) - Get Runscript execution status timedout" +} +function Collect-Log ($TargetSources, $FilesOrFolders) +{ + Begin + { + Write-Output "Executing Collect-Logs script" + Write-Output "TargetSources : $TargetSources, FilesOrFolders : $FilesOrFolders" + $TargetSourcesColletion = $TargetSources.Split(',', [StringSplitOptions]::RemoveEmptyEntries) + $FilesOrFoldersColletion = $FilesOrFolders.Split(',', [StringSplitOptions]::RemoveEmptyEntries).ToLowerInvariant() + $AllowedFolders = 'c:\windows\ccm\logs,c:\windows\ccmsetup\logs' + foreach ($Folder in $FilesOrFoldersColletion) + { + if ($AllowedFolders.Contains($Folder) -eq $false) + { + Write-Error "Folder : '$Folder' not in scope to collect device log" + break + } + } + #Connect to SCCM + Connect-ConfigMgrProvider -SiteCode $SiteCode -SiteServer $SiteServer + $TargetCollections = @() + $TargetDevices = @() + foreach ($TargetSource in $TargetSourcesColletion) + { + $Collection = Get-CMCollection -CollectionId $TargetSource + if ([string]::IsNullOrEmpty($Collection)) + { + $Device = Get-CMDevice -Name $TargetSource -Fast + if ([string]::IsNullOrEmpty($Device)) + { + Write-Error "'$TargetSource' is not valid Device or Collection" + } + else + { + $TargetDevices += $Device + } + } + else + { + $TargetCollections += $TargetSource + } + } + $SASToken = Get-SASToken -StorageAccountName $StorageAccountName -StorageAccountResourceGroup $StorageAccountResourceGroup + $RunScriptName = "Collect Logs to Cloud" #This file needs to be uploaded to SCCM + Write-Output "Getting CM Script '$RunScriptName' from ConfigManager" + $ScriptGuid = (Get-CMScript -ScriptName $RunScriptName -Fast).ScriptGuid + if ([string]::IsNullOrEmpty($ScriptGuid)) + { + Write-Error "No Script found with the name '$RunScriptName' in the ConfigMgr Script Repository" + break + } + } + Process + { + $EscapedSASToken = [uri]::EscapeDataString($SASToken) + $Parameters = @{ + "EscapedSASToken" = "$EscapedSASToken" + "StorageURL" = "$StorageURL" + "FilesOrFolders" = "$FilesOrFolders" + } + $ExecutionResults = @() + $CollectionOperationResults = @() + $DeviceOperationResults = @() + $CollectionExecutionStatuses = @() + $DeviceExecutionStatuses = @() + foreach ($TargetCollection in $TargetCollections) + { + Write-Output "Invoking Runscript on Config Manager for Collection $($TargetCollection)" + $OperationId = (Invoke-CMScript -CollectionId $TargetCollection -ScriptGuid $ScriptGuid -ScriptParameter $Parameters -PassThru).OperationID + Write-Output "Runscript Invoked on Config Manager for Collection $($TargetCollection) with OperationId: $($OperationId)" + $Record = New-Object PSObject + $Record | Add-Member -type NoteProperty -Name 'Collection' -Value $TargetCollection + $Record | Add-Member -type NoteProperty -Name 'OperationId' -Value $OperationId + $CollectionOperationResults += $Record + } + foreach ($TargetDevice in $TargetDevices) + { + Write-Output "Invoking Runscript on Config Manager for Device $($TargetDevice.Name)" + $OperationId = (Invoke-CMScript -Device $TargetDevice -ScriptGuid $ScriptGuid -ScriptParameter $Parameters -PassThru).OperationID + Write-Output "Runscript Invoked on Config Manager for Device $($TargetDevice.Name) with OperationId: $($OperationId)" + $Record = New-Object PSObject + $Record | Add-Member -type NoteProperty -Name 'Device' -Value $TargetDevice.Name + $Record | Add-Member -type NoteProperty -Name 'OperationId' -Value $OperationId + $DeviceOperationResults += $Record + } + # Check execution status + foreach ($DeviceOperation in $DeviceOperationResults) + { + $ExecutionStatuses = WaitToComplete -OperationId $DeviceOperation.OperationId + $Record = New-Object PSObject + $Record | Add-Member -type NoteProperty -Name 'Device' -Value $DeviceOperation.Device + $Record | Add-Member -type NoteProperty -Name 'OperationId' -Value $DeviceOperation.OperationId + $Record | Add-Member -type NoteProperty -Name 'ExecutionStatuses' -Value $ExecutionStatuses + $DeviceExecutionStatuses += $Record + } + foreach ($CollectionOperation in $CollectionOperationResults) + { + $ExecutionStatuses = WaitToComplete -OperationId $CollectionOperation.OperationId + $Record = New-Object PSObject + $Record | Add-Member -type NoteProperty -Name 'Collection' -Value $CollectionOperation.Collection + $Record | Add-Member -type NoteProperty -Name 'OperationId' -Value $CollectionOperation.OperationId + $Record | Add-Member -type NoteProperty -Name 'ExecutionStatuses' -Value $ExecutionStatuses + $CollectionExecutionStatuses += $Record + } + $Body = "RunScript : $RunScriptName, Target Source : $TargetSources" + #Prepare report for Collection + foreach ($CollectionExecutionStatus in $CollectionExecutionStatuses) + { + $ExecutionResults = @() + Write-Output "Fetching collection members" + $CollectionMembers = (Get-CMCollectionMember -CollectionId $CollectionExecutionStatus.Collection).Name + Write-Output "Checking Runscript execution status for each members for collection : $($CollectionExecutionStatus.Collection)" + foreach ($Member in $CollectionMembers) + { + $ExecutionStatus = $CollectionExecutionStatus.ExecutionStatuses | Where-Object { $_.DeviceName -eq $Member } + if ($ExecutionStatus -ne $null) + { + $ExecutionResults += [PSCustomObject] @{DeviceName = $($ExecutionStatus).DeviceName + ExecutionState = ([EnumExecutionState]$($ExecutionStatus).ScriptExecutionState).ToString() + ExitCode = $($ExecutionStatus).ScriptExitCode + ScriptOutput = $($ExecutionStatus).ScriptOutput + } + } + else + { + $ExecutionResults += [PSCustomObject] @{DeviceName = $Member + ExecutionState = 'Unknown' + ExitCode = 'Unknown' + ScriptOutput = 'NA' + } + } + } + $ExecTask = Get-CimInstance -ComputerName $SiteServer -Namespace $Namespace -ClassName SMS_ScriptsExecutionTask -Filter "ClientOperationID = '$($CollectionExecutionStatus.OperationId)'" + $OverAllResult = [PSCustomObject] @{Completed = $($ExecTask.CompletedClients) + NotApplicable = $($ExecTask.NotApplicableClients) + Failed = $($ExecTask.FailedClients) + Unknown = $($ExecTask.UnknownClients) + Offline = $($ExecTask.OfflineClients) + } + $OverAllResultTable = Create-HTMLTable -TableData $OverAllResult + $ExecutionResultTable = Create-HTMLTable -TableData $ExecutionResults + $Body += "

Overall Script Execution Status : Collection $($CollectionExecutionStatus.Collection)

" + $Body += $OverAllResultTable + $Body += "

Client Execution Status : Collection $($CollectionExecutionStatus.Collection)

" + $Body += $ExecutionResultTable + } + #Prepare report for Devices + foreach ($DeviceExecutionStatus in $DeviceExecutionStatuses) + { + $ExecutionResults = @() + $ExecutionStatus = $DeviceExecutionStatus.ExecutionStatuses + if ($ExecutionStatus -ne $null) + { + $ExecutionResults += [PSCustomObject] @{DeviceName = $($ExecutionStatus).DeviceName + ExecutionState = ([EnumExecutionState]$($ExecutionStatus).ScriptExecutionState).ToString() + ExitCode = $($ExecutionStatus).ScriptExitCode + ScriptOutput = $($ExecutionStatus).ScriptOutput + } + } + else + { + $ExecutionResults += [PSCustomObject] @{DeviceName = $Member + ExecutionState = 'Unknown' + ExitCode = 'Unknown' + ScriptOutput = 'NA' + } + } + $ExecutionResultTable = Create-HTMLTable -TableData $ExecutionResults + $Body += "

Individual Device Execution Status

" + $Body += $ExecutionResultTable + } + $Body += "

Files Uploaded to Storage Account : $($StorageURL)" + Write-Output "Collect-Logs Script Execution Completed" + $Body | ConvertTo-Html | Out-File -FilePath .\Collect-DeviceLogs-Report.html + } + End + { + Get-Content $global:LogFileName -ErrorAction SilentlyContinue + Remove-Item -Path $global:LogFileName -Force -ErrorAction SilentlyContinue + } +} +$TargetSources = "" #Comma separated list of SCCM Collection Ids or Device Names to collect logs from +$FilesOrFolders = "" #Comma separated list of Files/Folders Path to collect the logs from +Collect-Log -TargetSources $TargetSources -FilesOrFolders $FilesOrFolders \ No newline at end of file diff --git a/CollectSCCMDeviceLogsAutomation/docs/CollectDeviceLogs.jpg b/CollectSCCMDeviceLogsAutomation/docs/CollectDeviceLogs.jpg new file mode 100644 index 0000000..6e8af6c Binary files /dev/null and b/CollectSCCMDeviceLogsAutomation/docs/CollectDeviceLogs.jpg differ diff --git a/CollectSCCMDeviceLogsAutomation/docs/CollectDeviceLogs.md b/CollectSCCMDeviceLogsAutomation/docs/CollectDeviceLogs.md new file mode 100644 index 0000000..f403e75 --- /dev/null +++ b/CollectSCCMDeviceLogsAutomation/docs/CollectDeviceLogs.md @@ -0,0 +1,26 @@ +# SCCM Collect Device Logs Automation + +Powershell script used to collect device logs from user devices from SCCM. It works by connecting to SCCM, running a Powershell script called Collect Logs to Cloud stored in SCCM on each device collecting specific logs, connecting to an Azure Storage Account with an MSI using Client Id, and then sending the logs to the Storage Account. An HTML Report is also generated in the local filepath with the results. + +## Prerequisites + +- SCCM script "Collect Logs to Cloud" must be uploaded to SCCM +- Az Powershell module installed +- Azure Storage Account set up with MSI Access +- Powershell ISE + +### Steps: + +1. Open PowerShell ISE in elevated mode and open the following script: Collect-DeviceLogs.ps1 +2. Manually configure the following variables for your own environment: + - SiteCode: SCCM Site Code + - SiteServer: SCCM Site Server + - StorageAccountName: Name of Azure Storage Account + - StorageAccountResourceGroup: Name of Azure Storage Account Resource Group + - Container: Name of Container in Azure Storage Account + - ClientId: Azure MSI Client Id used to get the Storage Account Access Token +3. Run the Powershell script + +## Notes + +The SCCM Collect Device Logs Automation script was originally created for use inside of Microsoft. We have modified it to be more generic, so it can be used as a template for other SCCM environments outside of Microsoft. diff --git a/ProvisionWin32AppAutomation/ProvisionWin32App/Provision-Win32App.ps1 b/ProvisionWin32AppAutomation/ProvisionWin32App/Provision-Win32App.ps1 new file mode 100644 index 0000000..a5fc5cd --- /dev/null +++ b/ProvisionWin32AppAutomation/ProvisionWin32App/Provision-Win32App.ps1 @@ -0,0 +1,898 @@ +<# +.SYNOPSIS + Provision Win 32 App to be uploaded to Intune +.DESCRIPTION + Powershell script used to upload a win32 app into Intune. + It works by getting a Graph API token using an MSI Client Id + and then uploads the intunewin file into Intune +.PARAMETER ClientId + Azure MSI Client Id used to get the Graph API access token +.PARAMETER SourceFile + This is the path to the Intunewin file +.PARAMETER Publisher + The publisher of the application +.PARAMETER Description + Description of the application +.PARAMETER PowerShellDetectionScript + This is the path to the detection script +.NOTES + Version: 1.0 + Creation date: 2/15/2023 + Purpose/Change: Open Source example +#> + +function Get-MsiAuthHeaders +{ + param + ( + [Parameter(Mandatory=$true)] + [Validateset("https://graph.microsoft.com","https://vault.azure.net","https://management.azure.com","https://database.windows.net")] + $ResourceUri, + [Parameter(Mandatory=$true)] + $ClientId + ) + + $msiEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=$($ResourceUri)&client_id=$($ClientId)" + $response = Invoke-WebRequest -Uri $msiEndpoint -Headers @{"Metadata"= $true} -UseBasicParsing + $responseJson = ConvertFrom-Json -InputObject $response + $AccessToken = $responseJson.access_token + $Headers = @{ + 'Content-Type'='application/json' + 'Authorization'="Bearer " + $AccessToken + } + return $Headers +} +  +function CloneObject($object) +{ + $stream = New-Object IO.MemoryStream; + $formatter = New-Object Runtime.Serialization.Formatters.Binary.BinaryFormatter; + $formatter.Serialize($stream, $object); + $stream.Position = 0; + $formatter.Deserialize($stream); +} + +function WriteHeaders($headers) +{ + foreach ($header in $headers.GetEnumerator()) + { + if ($header.Name.ToLower() -eq "authorization") + { + continue; + } + Write-Host -ForegroundColor Gray "$($header.Name): $($header.Value)"; + } +} + +function MakeGetRequest($collectionPath) +{ + $uri = "$baseUrl$collectionPath"; + $request = "GET $uri"; + + if ($logRequestUris) { Write-Host $request; } + if ($logHeaders) { WriteHeaders $headers; } + + try + { + $response = Invoke-RestMethod $uri -Method Get -Headers $headers; + $response; + } + catch + { + Write-Host -ForegroundColor Red $request; + Write-Host -ForegroundColor Red $_.Exception.Message; + throw; + } +} + +function MakePatchRequest($collectionPath, $body) +{ + MakeRequest "PATCH" $collectionPath $body; +} + +function MakePostRequest($collectionPath, $body) +{ + MakeRequest "POST" $collectionPath $body; +} + +function MakeRequest($verb, $collectionPath, $body) +{ + $uri = "$baseUrl$collectionPath"; + $request = "$verb $uri"; + + $clonedHeaders = CloneObject $headers; + $clonedHeaders["content-length"] = $body.Length; + $clonedHeaders["content-type"] = "application/json"; + + if ($logRequestUris) { Write-Host $request; } + if ($logHeaders) { WriteHeaders $clonedHeaders; } + if ($logContent) { Write-Host -ForegroundColor Gray $body; } + + try + { + $response = Invoke-RestMethod $uri -Method $verb -Headers $clonedHeaders -Body $body; + $response; + } + catch + { + Write-Host -ForegroundColor Red $request; + Write-Host -ForegroundColor Red $_.Exception.Message; + throw; + } +} + +function UploadAzureStorageChunk($sasUri, $id, $body) +{ + $uri = "$sasUri&comp=block&blockid=$id"; + $request = "PUT $uri"; + + $iso = [System.Text.Encoding]::GetEncoding("iso-8859-1"); + $encodedBody = $iso.GetString($body); + $headers = @{ + "x-ms-blob-type" = "BlockBlob" + }; + + if ($logRequestUris) { Write-Host $request; } + if ($logHeaders) { WriteHeaders $headers; } + + try + { + $response = Invoke-WebRequest $uri -Method Put -Headers $headers -Body $encodedBody; + } + catch + { + Write-Host -ForegroundColor Red $request; + Write-Host -ForegroundColor Red $_.Exception.Message; + throw; + } +} + +function FinalizeAzureStorageUpload($sasUri, $ids) +{ + $uri = "$sasUri&comp=blocklist"; + $request = "PUT $uri"; + + $xml = ''; + foreach ($id in $ids) + { + $xml += "$id"; + } + $xml += ''; + + if ($logRequestUris) { Write-Host $request; } + if ($logContent) { Write-Host -ForegroundColor Gray $xml; } + + try + { + Invoke-RestMethod $uri -Method Put -Body $xml; + } + catch + { + Write-Host -ForegroundColor Red $request; + Write-Host -ForegroundColor Red $_.Exception.Message; + throw; + } +} + +function UploadFileToAzureStorage($sasUri, $filepath, $fileUri) +{ + try { + + $chunkSizeInBytes = 1024l * 1024l * $azureStorageUploadChunkSizeInMb; + + # Start the timer for SAS URI renewal. + $sasRenewalTimer = [System.Diagnostics.Stopwatch]::StartNew() + + # Find the file size and open the file. + $fileSize = (Get-Item $filepath).length; + $chunks = [Math]::Ceiling($fileSize / $chunkSizeInBytes); + $reader = New-Object System.IO.BinaryReader([System.IO.File]::Open($filepath, [System.IO.FileMode]::Open)); + $position = $reader.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. + $ids = @(); + + for ($chunk = 0; $chunk -lt $chunks; $chunk++){ + + $id = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($chunk.ToString("0000"))); + $ids += $id; + + $start = $chunk * $chunkSizeInBytes; + $length = [Math]::Min($chunkSizeInBytes, $fileSize - $start); + $bytes = $reader.ReadBytes($length); + + $currentChunk = $chunk + 1; + + Write-Progress -Activity "Uploading File to Azure Storage" -status "Uploading chunk $currentChunk of $chunks" ` + -percentComplete ($currentChunk / $chunks*100) + + $uploadResponse = UploadAzureStorageChunk $sasUri $id $bytes; + + # Renew the SAS URI if 7 minutes have elapsed since the upload started or was renewed last. + if ($currentChunk -lt $chunks -and $sasRenewalTimer.ElapsedMilliseconds -ge 450000){ + $renewalResponse = RenewAzureStorageUpload $fileUri; + $sasRenewalTimer.Restart(); + } + } + + Write-Progress -Completed -Activity "Uploading File to Azure Storage" + + $reader.Close(); + } + finally { + if ($reader -ne $null) { $reader.Dispose(); } + } + + # Finalize the upload. + $uploadResponse = FinalizeAzureStorageUpload $sasUri $ids; +} + +function RenewAzureStorageUpload($fileUri) +{ + $renewalUri = "$fileUri/renewUpload"; + $actionBody = ""; + $rewnewUriResult = MakePostRequest $renewalUri $actionBody; + $file = WaitForFileProcessing $fileUri "AzureStorageUriRenewal" $azureStorageRenewSasUriBackOffTimeInSeconds; +} + +function WaitForFileProcessing($fileUri, $stage) +{ + $attempts= 60; + $waitTimeInSeconds = 1; + + $successState = "$($stage)Success"; + $pendingState = "$($stage)Pending"; + $failedState = "$($stage)Failed"; + $timedOutState = "$($stage)TimedOut"; + + $file = $null; + while ($attempts -gt 0) + { + $file = MakeGetRequest $fileUri; + + if ($file.uploadState -eq $successState) + { + break; + } + elseif ($file.uploadState -ne $pendingState) + { + Write-Host -ForegroundColor Red $_.Exception.Message; + throw "File upload state is not success: $($file.uploadState)"; + } + + Start-Sleep $waitTimeInSeconds; + $attempts--; + } + + if ($file -eq $null -or $file.uploadState -ne $successState) + { + throw "File request did not complete in the allotted time."; + } + + $file; +} + +function GetWin32AppBody() +{ +param +( + +[parameter(Mandatory=$true,ParameterSetName = "MSI",Position=1)] +[Switch]$MSI, + +[parameter(Mandatory=$true,ParameterSetName = "EXE",Position=1)] +[Switch]$EXE, + +[parameter(Mandatory=$true)] +[ValidateNotNullOrEmpty()] +[string]$displayName, + +[parameter(Mandatory=$true)] +[ValidateNotNullOrEmpty()] +[string]$publisher, + +[parameter(Mandatory=$true)] +[ValidateNotNullOrEmpty()] +[string]$description, + +[parameter(Mandatory=$true)] +[ValidateNotNullOrEmpty()] +[string]$filename, + +[parameter(Mandatory=$true)] +[ValidateNotNullOrEmpty()] +[string]$SetupFileName, + +[parameter(Mandatory=$true)] +[ValidateSet('system','user')] +$installExperience = "system", + +[parameter(Mandatory=$true,ParameterSetName = "EXE")] +[ValidateNotNullOrEmpty()] +$installCommandLine, + +[parameter(Mandatory=$true,ParameterSetName = "EXE")] +[ValidateNotNullOrEmpty()] +$uninstallCommandLine, + +[parameter(Mandatory=$true,ParameterSetName = "MSI")] +[ValidateNotNullOrEmpty()] +$MsiPackageType, + +[parameter(Mandatory=$true,ParameterSetName = "MSI")] +[ValidateNotNullOrEmpty()] +$MsiProductCode, + +[parameter(Mandatory=$false,ParameterSetName = "MSI")] +$MsiProductName, + +[parameter(Mandatory=$true,ParameterSetName = "MSI")] +[ValidateNotNullOrEmpty()] +$MsiProductVersion, + +[parameter(Mandatory=$false,ParameterSetName = "MSI")] +$MsiPublisher, + +[parameter(Mandatory=$true,ParameterSetName = "MSI")] +[ValidateNotNullOrEmpty()] +$MsiRequiresReboot, + +[parameter(Mandatory=$true,ParameterSetName = "MSI")] +[ValidateNotNullOrEmpty()] +$MsiUpgradeCode + +) + + if($MSI){ + + $body = @{ "@odata.type" = "#microsoft.graph.win32LobApp" }; + $body.applicableArchitectures = "x64,x86"; + $body.description = $description; + $body.developer = ""; + $body.displayName = $displayName; + $body.fileName = $filename; + $body.installCommandLine = "msiexec /i `"$SetupFileName`"" + $body.installExperience = @{"runAsAccount" = "$installExperience"}; + $body.informationUrl = $null; + $body.isFeatured = $false; + $body.minimumSupportedOperatingSystem = @{"v10_1607" = $true}; + $body.msiInformation = @{ + "packageType" = "$MsiPackageType"; + "productCode" = "$MsiProductCode"; + "productName" = "$MsiProductName"; + "productVersion" = "$MsiProductVersion"; + "publisher" = "$MsiPublisher"; + "requiresReboot" = "$MsiRequiresReboot"; + "upgradeCode" = "$MsiUpgradeCode" + }; + $body.notes = ""; + $body.owner = ""; + $body.privacyInformationUrl = $null; + $body.publisher = $publisher; + $body.runAs32bit = $false; + $body.setupFilePath = $SetupFileName; + $body.uninstallCommandLine = "msiexec /x `"$MsiProductCode`"" + + } + elseif($EXE){ + $body = @{ "@odata.type" = "#microsoft.graph.win32LobApp" }; + $body.description = $description; + $body.developer = ""; + $body.displayName = $displayName; + $body.fileName = $filename; + $body.installCommandLine = "$installCommandLine" + $body.installExperience = @{"runAsAccount" = "$installExperience"}; + $body.informationUrl = $null; + $body.isFeatured = $false; + $body.minimumSupportedOperatingSystem = @{"v10_1607" = $true}; + $body.msiInformation = $null; + $body.notes = ""; + $body.owner = ""; + $body.privacyInformationUrl = $null; + $body.publisher = $publisher; + $body.runAs32bit = $false; + $body.setupFilePath = $SetupFileName; + $body.uninstallCommandLine = "$uninstallCommandLine" + } + $body; +} + +function GetAppFileBody($name, $size, $sizeEncrypted, $manifest) +{ + $body = @{ "@odata.type" = "#microsoft.graph.mobileAppContentFile" }; + $body.name = $name; + $body.size = $size; + $body.sizeEncrypted = $sizeEncrypted; + $body.manifest = $manifest; + $body.isDependency = $false; + $body; +} + +function GetAppCommitBody($contentVersionId, $LobType) +{ + $body = @{ "@odata.type" = "#$LobType" }; + $body.committedContentVersion = $contentVersionId; + $body; +} + +Function Test-SourceFile() +{ +param +( +    [parameter(Mandatory=$true)] +    [ValidateNotNullOrEmpty()] + $SourceFile +) + + try { + if(!(test-path "$SourceFile")){ + Write-Host "Source File '$sourceFile' doesn't exist..." -ForegroundColor Red + throw + } + } + + catch { + Write-Host -ForegroundColor Red $_.Exception.Message; + break + } +} + +Function New-DetectionRule() +{ +[cmdletbinding()] + +param +( + [parameter(Mandatory=$true,ParameterSetName = "PowerShell",Position=1)] + [Switch]$PowerShell, + + [parameter(Mandatory=$true,ParameterSetName = "MSI",Position=1)] + [Switch]$MSI, + + [parameter(Mandatory=$true,ParameterSetName = "File",Position=1)] + [Switch]$File, + + [parameter(Mandatory=$true,ParameterSetName = "Registry",Position=1)] + [Switch]$Registry, + + [parameter(Mandatory=$true,ParameterSetName = "PowerShell")] + [ValidateNotNullOrEmpty()] + [String]$ScriptFile, + + [parameter(Mandatory=$true,ParameterSetName = "PowerShell")] + [ValidateNotNullOrEmpty()] + $enforceSignatureCheck, + + [parameter(Mandatory=$true,ParameterSetName = "PowerShell")] + [ValidateNotNullOrEmpty()] + $runAs32Bit, + + [parameter(Mandatory=$true,ParameterSetName = "MSI")] + [ValidateNotNullOrEmpty()] + [String]$MSIproductCode, + + [parameter(Mandatory=$true,ParameterSetName = "File")] + [ValidateNotNullOrEmpty()] + [String]$Path, + + [parameter(Mandatory=$true,ParameterSetName = "File")] + [ValidateNotNullOrEmpty()] + [string]$FileOrFolderName, + + [parameter(Mandatory=$true,ParameterSetName = "File")] + [ValidateSet("notConfigured","exists","modifiedDate","createdDate","version","sizeInMB")] + [string]$FileDetectionType, + + [parameter(Mandatory=$false,ParameterSetName = "File")] + $FileDetectionValue = $null, + + [parameter(Mandatory=$true,ParameterSetName = "File")] + [ValidateSet("True","False")] + [string]$check32BitOn64System = "False", + + [parameter(Mandatory=$true,ParameterSetName = "Registry")] + [ValidateNotNullOrEmpty()] + [String]$RegistryKeyPath, + + [parameter(Mandatory=$true,ParameterSetName = "Registry")] + [ValidateSet("notConfigured","exists","doesNotExist","string","integer","version")] + [string]$RegistryDetectionType, + + [parameter(Mandatory=$false,ParameterSetName = "Registry")] + [ValidateNotNullOrEmpty()] + [String]$RegistryValue, + + [parameter(Mandatory=$true,ParameterSetName = "Registry")] + [ValidateSet("True","False")] + [string]$check32BitRegOn64System = "False" + +) + + if($PowerShell){ + + if(!(Test-Path "$ScriptFile")){ + Write-Host "Could not find file '$ScriptFile'..." -ForegroundColor Red + Write-Host "Script can't continue..." -ForegroundColor Red + Write-Host + break + } + + $ScriptContent = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("$ScriptFile")); + + $DR = @{ "@odata.type" = "#microsoft.graph.win32LobAppPowerShellScriptDetection" } + $DR.enforceSignatureCheck = $false; + $DR.runAs32Bit = $false; + $DR.scriptContent = "$ScriptContent"; + } + + elseif($MSI){ + $DR = @{ "@odata.type" = "#microsoft.graph.win32LobAppProductCodeDetection" } + $DR.productVersionOperator = "notConfigured"; + $DR.productCode = "$MsiProductCode"; + $DR.productVersion = $null; + } + + elseif($File){ + $DR = @{ "@odata.type" = "#microsoft.graph.win32LobAppFileSystemDetection" } + $DR.check32BitOn64System = "$check32BitOn64System"; + $DR.detectionType = "$FileDetectionType"; + $DR.detectionValue = $FileDetectionValue; + $DR.fileOrFolderName = "$FileOrFolderName"; + $DR.operator = "notConfigured"; + $DR.path = "$Path" + } + + elseif($Registry){ + $DR = @{ "@odata.type" = "#microsoft.graph.win32LobAppRegistryDetection" } + $DR.check32BitOn64System = "$check32BitRegOn64System"; + $DR.detectionType = "$RegistryDetectionType"; + $DR.detectionValue = ""; + $DR.keyPath = "$RegistryKeyPath"; + $DR.operator = "notConfigured"; + $DR.valueName = "$RegistryValue" + } + + return $DR +} + +function Get-DefaultReturnCodes() +{ +@{"returnCode" = 0;"type" = "success"}, ` +@{"returnCode" = 1707;"type" = "success"}, ` +@{"returnCode" = 3010;"type" = "softReboot"}, ` +@{"returnCode" = 1641;"type" = "hardReboot"}, ` +@{"returnCode" = 1618;"type" = "retry"} +} + +function New-ReturnCode() +{ +param +( +[parameter(Mandatory=$true)] +[int]$returnCode, +[parameter(Mandatory=$true)] +[ValidateSet('success','softReboot','hardReboot','retry')] +$type +) + + @{"returnCode" = $returnCode;"type" = "$type"} +} + +Function Get-IntuneWinXML() +{ +param +( +[Parameter(Mandatory=$true)] +$SourceFile, + +[Parameter(Mandatory=$true)] +$fileName, + +[Parameter(Mandatory=$false)] +[ValidateSet("false","true")] +[string]$removeitem = "true" +) + +Test-SourceFile "$SourceFile" + +$Directory = [System.IO.Path]::GetDirectoryName("$SourceFile") + +Add-Type -Assembly System.IO.Compression.FileSystem +$zip = [IO.Compression.ZipFile]::OpenRead("$SourceFile") + + $zip.Entries | where {$_.Name -like "$filename" } | foreach { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, "$Directory\$filename", $true) + } + +$zip.Dispose() + +[xml]$IntuneWinXML = gc "$Directory\$filename" + +return $IntuneWinXML + +if($removeitem -eq "true"){ remove-item "$Directory\$filename" } +} + +Function Get-IntuneWinFile() +{ +param +( +[Parameter(Mandatory=$true)] +$SourceFile, + +[Parameter(Mandatory=$true)] +$fileName, + +[Parameter(Mandatory=$false)] +[string]$Folder = "win32" +) + + $Directory = [System.IO.Path]::GetDirectoryName("$SourceFile") + + if(!(Test-Path "$Directory\$folder")){ + + New-Item -ItemType Directory -Path "$Directory" -Name "$folder" | Out-Null + + } + + Add-Type -Assembly System.IO.Compression.FileSystem + $zip = [IO.Compression.ZipFile]::OpenRead("$SourceFile") + + $zip.Entries | where {$_.Name -like "$filename" } | foreach { + + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, "$Directory\$folder\$filename", $true) + + } + + $zip.Dispose() + + return "$Directory\$folder\$filename" + + if($removeitem -eq "true"){ remove-item "$Directory\$filename" } +} + +function Upload-Win32Lob() +{ +<# +.SYNOPSIS +This function is used to upload a Win32 Application to the Intune Service +.DESCRIPTION +This function is used to upload a Win32 Application to the Intune Service +.EXAMPLE +Upload-Win32Lob "C:\Packages\package.intunewin" -publisher "Microsoft" -description "Package" +This example uses all parameters required to add an intunewin File into the Intune Service +.EXAMPLE +Upload-Win32Lob -SourceFile "$SourceFile" -displayName "Application Name" -publisher "Publisher" -description "Description" -detectionRules $DetectionRule -returnCodes $ReturnCodes +-installCmdLine "powershell.exe -executionpolicy Bypass .\install.ps1" -uninstallCmdLine "powershell.exe -executionpolicy Bypass .\uninstall.ps1" +This example uses additional parameters +#> + +[cmdletbinding()] + +param +( +    [parameter(Mandatory=$true,Position=1)] +    [ValidateNotNullOrEmpty()] + [string]$SourceFile, + + [parameter(Mandatory=$false)] +    [ValidateNotNullOrEmpty()] + [string]$displayName, + + [parameter(Mandatory=$true,Position=2)] +    [ValidateNotNullOrEmpty()] + [string]$publisher, + + [parameter(Mandatory=$true,Position=3)] +    [ValidateNotNullOrEmpty()] + [string]$description, + + [parameter(Mandatory=$true,Position=4)] +    [ValidateNotNullOrEmpty()] + $detectionRules, + + [parameter(Mandatory=$true,Position=5)] +    [ValidateNotNullOrEmpty()] + $returnCodes, + + [parameter(Mandatory=$false,Position=6)] +    [ValidateNotNullOrEmpty()] + [string]$installCmdLine, + + [parameter(Mandatory=$false,Position=7)] +    [ValidateNotNullOrEmpty()] + [string]$uninstallCmdLine, + + [parameter(Mandatory=$false,Position=8)] + [ValidateSet('system','user')] + $installExperience = "system" +) + + try { + $LOBType = "microsoft.graph.win32LobApp" + + Write-Host "Testing if SourceFile '$SourceFile' Path is valid..." -ForegroundColor Yellow + Test-SourceFile "$SourceFile" + + $Win32Path = "$SourceFile" + + Write-Host "Creating JSON data to pass to the service..." -ForegroundColor Yellow + + # Funciton to read Win32LOB file + $DetectionXML = Get-IntuneWinXML "$SourceFile" -fileName "detection.xml" + + # If displayName input don't use Name from detection.xml file + if($displayName){ $DisplayName = $displayName } + else { $DisplayName = $DetectionXML.ApplicationInfo.Name } + + $FileName = $DetectionXML.ApplicationInfo.FileName + + $SetupFileName = $DetectionXML.ApplicationInfo.SetupFile + + $Ext = [System.IO.Path]::GetExtension($SetupFileName) + + if((($Ext).contains("msi") -or ($Ext).contains("Msi")) -and (!$installCmdLine -or !$uninstallCmdLine)){ + + # MSI + $MsiExecutionContext = $DetectionXML.ApplicationInfo.MsiInfo.MsiExecutionContext + $MsiPackageType = "DualPurpose"; + if($MsiExecutionContext -eq "System") { $MsiPackageType = "PerMachine" } + elseif($MsiExecutionContext -eq "User") { $MsiPackageType = "PerUser" } + + $MsiProductCode = $DetectionXML.ApplicationInfo.MsiInfo.MsiProductCode + $MsiProductVersion = $DetectionXML.ApplicationInfo.MsiInfo.MsiProductVersion + $MsiPublisher = $DetectionXML.ApplicationInfo.MsiInfo.MsiPublisher + $MsiRequiresReboot = $DetectionXML.ApplicationInfo.MsiInfo.MsiRequiresReboot + $MsiUpgradeCode = $DetectionXML.ApplicationInfo.MsiInfo.MsiUpgradeCode + + if($MsiRequiresReboot -eq "false"){ $MsiRequiresReboot = $false } + elseif($MsiRequiresReboot -eq "true"){ $MsiRequiresReboot = $true } + + $mobileAppBody = GetWin32AppBody ` + -MSI ` + -displayName "$DisplayName" ` + -publisher "$publisher" ` + -description $description ` + -filename $FileName ` + -SetupFileName "$SetupFileName" ` + -installExperience $installExperience ` + -MsiPackageType $MsiPackageType ` + -MsiProductCode $MsiProductCode ` + -MsiProductName $displayName ` + -MsiProductVersion $MsiProductVersion ` + -MsiPublisher $MsiPublisher ` + -MsiRequiresReboot $MsiRequiresReboot ` + -MsiUpgradeCode $MsiUpgradeCode + } + else { + $mobileAppBody = GetWin32AppBody -EXE -displayName "$DisplayName" -publisher "$publisher" ` + -description $description -filename $FileName -SetupFileName "$SetupFileName" ` + -installExperience $installExperience -installCommandLine $installCmdLine ` + -uninstallCommandLine $uninstallcmdline + } + + if($DetectionRules.'@odata.type' -contains "#microsoft.graph.win32LobAppPowerShellScriptDetection" -and @($DetectionRules).'@odata.type'.Count -gt 1){ + Write-Warning "A Detection Rule can either be 'Manually configure detection rules' or 'Use a custom detection script'" + Write-Warning "It can't include both..." + break + } + else { + $mobileAppBody | Add-Member -MemberType NoteProperty -Name 'detectionRules' -Value $detectionRules + } + + #ReturnCodes + + if($returnCodes){ + $mobileAppBody | Add-Member -MemberType NoteProperty -Name 'returnCodes' -Value @($returnCodes) + } + else { + Write-Warning "Intunewin file requires ReturnCodes to be specified" + Write-Warning "If you want to use the default ReturnCode run 'Get-DefaultReturnCodes'" + break + } + + Write-Host "Creating application in Intune..." -ForegroundColor Yellow + $mobileApp = MakePostRequest "mobileApps" ($mobileAppBody | ConvertTo-Json); + + # Get the content version for the new app (this will always be 1 until the new app is committed). + Write-Host "Creating Content Version in the service for the application..." -ForegroundColor Yellow + $appId = $mobileApp.id; + $contentVersionUri = "mobileApps/$appId/$LOBType/contentVersions"; + $contentVersion = MakePostRequest $contentVersionUri "{}"; + + # Encrypt file and Get File Information + Write-Host "Getting Encryption Information for '$SourceFile'..." -ForegroundColor Yellow + + $encryptionInfo = @{}; + $encryptionInfo.encryptionKey = $DetectionXML.ApplicationInfo.EncryptionInfo.EncryptionKey + $encryptionInfo.macKey = $DetectionXML.ApplicationInfo.EncryptionInfo.macKey + $encryptionInfo.initializationVector = $DetectionXML.ApplicationInfo.EncryptionInfo.initializationVector + $encryptionInfo.mac = $DetectionXML.ApplicationInfo.EncryptionInfo.mac + $encryptionInfo.profileIdentifier = "ProfileVersion1"; + $encryptionInfo.fileDigest = $DetectionXML.ApplicationInfo.EncryptionInfo.fileDigest + $encryptionInfo.fileDigestAlgorithm = $DetectionXML.ApplicationInfo.EncryptionInfo.fileDigestAlgorithm + + $fileEncryptionInfo = @{}; + $fileEncryptionInfo.fileEncryptionInfo = $encryptionInfo; + + # Extracting encrypted file + $IntuneWinFile = Get-IntuneWinFile "$SourceFile" -fileName "$filename" + + [int64]$Size = $DetectionXML.ApplicationInfo.UnencryptedContentSize + $EncrySize = (Get-Item "$IntuneWinFile").Length + + # Create a new file for the app. + Write-Host "Creating a new file entry in Azure for the upload..." -ForegroundColor Yellow + $contentVersionId = $contentVersion.id; + $fileBody = GetAppFileBody "$FileName" $Size $EncrySize $null; + $filesUri = "mobileApps/$appId/$LOBType/contentVersions/$contentVersionId/files"; + $file = MakePostRequest $filesUri ($fileBody | ConvertTo-Json); + + # Wait for the service to process the new file request. + Write-Host "Waiting for the file entry URI to be created..." -ForegroundColor Yellow + $fileId = $file.id; + $fileUri = "mobileApps/$appId/$LOBType/contentVersions/$contentVersionId/files/$fileId"; + $file = WaitForFileProcessing $fileUri "AzureStorageUriRequest"; + + # Upload the content to Azure Storage. + Write-Host "Uploading file to Azure Storage..." -f Yellow + + $sasUri = $file.azureStorageUri; + UploadFileToAzureStorage $file.azureStorageUri "$IntuneWinFile" $fileUri; + + # Need to Add removal of IntuneWin file + $IntuneWinFolder = [System.IO.Path]::GetDirectoryName("$IntuneWinFile") + Remove-Item "$IntuneWinFile" -Force + + # Commit the file. + Write-Host "Committing the file into Azure Storage..." -ForegroundColor Yellow + $commitFileUri = "mobileApps/$appId/$LOBType/contentVersions/$contentVersionId/files/$fileId/commit"; + MakePostRequest $commitFileUri ($fileEncryptionInfo | ConvertTo-Json); + + # Wait for the service to process the commit file request. + Write-Host "Waiting for the service to process the commit file request..." -ForegroundColor Yellow + $file = WaitForFileProcessing $fileUri "CommitFile"; + + # Commit the app. + Write-Host "Committing the file into Azure Storage..." -ForegroundColor Yellow + $commitAppUri = "mobileApps/$appId"; + $commitAppBody = GetAppCommitBody $contentVersionId $LOBType; + MakePatchRequest $commitAppUri ($commitAppBody | ConvertTo-Json); + + Write-Host "Sleeping for $sleep seconds to allow patch completion..." -f Magenta + Start-Sleep $sleep + Write-Host + } + catch { + Write-Host ""; + Write-Host -ForegroundColor Red "Aborting with exception: $($_.Exception.ToString())"; + } +} + +$ClientId = "" #Enter your Client Id for getting MS Graph access token with permission: DeviceManagementApps.ReadWrite.All +$global:headers = Get-MsiAuthHeaders -resourceUri "https://graph.microsoft.com" -clientId $ClientId + +$baseUrl = "https://graph.microsoft.com/beta/deviceAppManagement/" + +$logRequestUris = $true; +$logHeaders = $false; +$logContent = $true; + +$azureStorageUploadChunkSizeInMb = 6l; + +$sleep = 30 + +$SourceFile = "" #Full path to .intunewin file +$PowerShellDetectionScript = "" #Full path to Powershell Detection script +$PowerShellDetectionRule = New-DetectionRule -PowerShell -ScriptFile "$PowerShellDetectionScript" -enforceSignatureCheck $false -runAs32Bit $true + +$Publisher = "" #Publisher of app +$Description = "" #Description of app + +# Creating Array for detection Rule +$DetectionRule = @($PowerShellDetectionRule) + +$ReturnCodes = Get-DefaultReturnCodes + +# Win32 Application Upload +Upload-Win32Lob -SourceFile "$SourceFile" -publisher $Publisher -description $Description -detectionRules $DetectionRule -returnCodes $ReturnCodes \ No newline at end of file diff --git a/ProvisionWin32AppAutomation/docs/CreateIntunewinFile.png b/ProvisionWin32AppAutomation/docs/CreateIntunewinFile.png new file mode 100644 index 0000000..610e34b Binary files /dev/null and b/ProvisionWin32AppAutomation/docs/CreateIntunewinFile.png differ diff --git a/ProvisionWin32AppAutomation/docs/ProvisionWin32App.jpg b/ProvisionWin32AppAutomation/docs/ProvisionWin32App.jpg new file mode 100644 index 0000000..f020024 Binary files /dev/null and b/ProvisionWin32AppAutomation/docs/ProvisionWin32App.jpg differ diff --git a/ProvisionWin32AppAutomation/docs/ProvisionWin32App.md b/ProvisionWin32AppAutomation/docs/ProvisionWin32App.md new file mode 100644 index 0000000..a609520 --- /dev/null +++ b/ProvisionWin32AppAutomation/docs/ProvisionWin32App.md @@ -0,0 +1,29 @@ +# Provision Win32 App + +Powershell script used to Provision a Win 32 App to be uploaded to Intune. It works by getting a Graph API token using an MSI Client Id and then uploads the intunewin file into Intune. + +## Prerequisites + +- Windows 10 version 1607 or later (Enterprise, Pro, and Education versions) +- Client should have Graph API permission: **DeviceManagementApps.ReadWrite.All** +- Powershell ISE +- Microsoft Win32 Content Prep Tool +- Application file being uploaded to Intune + +### Steps: + +1. Download Microsoft Win32 Content Prep Tool +2. Run the IntuneWinAppUtil.exe and specify the following parameters: folder with setup files, path to .exe or .msi application file, and the output folder to send the .intunewin file +3. Open PowerShell ISE in elevated mode and open the following script: Provision-Win32App.ps1 +4. Manually configure the following variables for your own environment: + - ClientId: Azure MSI Client Id used to get the Graph API access token + - SourceFile: This is the path to the Intunewin file + - PowerShellDetectionScript: This is the path to the detection script + - Publisher: The publisher of the application + - Description: Description of the application +5. Modify New-DetectionRule, Get-DefaultReturnCodes, and Upload-Win32Lob function parameters as needed based on requirmements +6. Run the Powershell script + +## Notes + +The Provision Win32 App script was originally created for use inside of Microsoft. We have modified it to be more generic, so it can be used as a template for other Intune environments outside of Microsoft. diff --git a/VerifyDeletionAutomation/VerifyDeleteIntuneObjects/Verify-DeleteIntuneObjects.ps1 b/VerifyDeletionAutomation/VerifyDeleteIntuneObjects/Verify-DeleteIntuneObjects.ps1 new file mode 100644 index 0000000..d9f5028 --- /dev/null +++ b/VerifyDeletionAutomation/VerifyDeleteIntuneObjects/Verify-DeleteIntuneObjects.ps1 @@ -0,0 +1,160 @@ +<# +.SYNOPSIS + Verify Delete Intune Mobile Apps, Device Configuration Profiles, and Group Policy Configurations +.DESCRIPTION + Powershell script used to verify objects exist in Intune before deletion. + It works by getting a Graph API token using an MSI Client Id + and verifies Intune objects exist using the Graph API. +.PARAMETER MobileAppClientId + Azure MSI Client Id used to get the Graph API access token for Intune Mobile Apps +.PARAMETER DeviceConfigProfileClientId + Azure MSI Client Id used to get the Graph API access token for Intune Device Configuration Profiles +.PARAMETER GroupPolicyClientId + Azure MSI Client Id used to get the Graph API access token for Intune Group Policy Configurations +.PARAMETER MobileAppId + The Intune Mobile Application Id being verified +.PARAMETER DeviceConfigurationProfileId + The Intune Device Configuration Profile Id being verified +.PARAMETER GroupPolicyConfigurationId + The Intune Group Policy Configuration Id being verified +.NOTES + Version: 1.0 + Creation date: 2/6/2023 + Purpose/Change: Open Source example +#> + +function Get-MsiAuthHeaders +{ + param + ( + [Parameter(Mandatory=$true)] + [Validateset("https://graph.microsoft.com","https://vault.azure.net","https://management.azure.com","https://database.windows.net")] + $ResourceUri, + [Parameter(Mandatory=$true)] + $ClientId + ) + + $msiEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=$($ResourceUri)&client_id=$($ClientId)" + $response = Invoke-WebRequest -Uri $msiEndpoint -Headers @{"Metadata"= $true} -UseBasicParsing + $responseJson = ConvertFrom-Json -InputObject $response + $AccessToken = $responseJson.access_token + $Headers = @{ + 'Content-Type'='application/json' + 'Authorization'="Bearer " + $AccessToken + } + return $Headers +} + +function Verify-IntuneMobileApp { + param( + $MobileAppId + ) + try + { + $MobileAppClientId = "" #Enter your Client Id for getting access token + #This Client Id must have Graph API permission: DeviceManagementApps.Read.All + + Write-Output "Getting microsoft graph api access headers using client id - $($MobileAppClientId)" + $AppHeaders = Get-MsiAuthHeaders -ResourceUri "https://graph.microsoft.com" -ClientId $MobileAppClientId + $AppGraphUrl = "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$($mobileAppId)" + Write-Output "Getting Mobile app using Url: $($AppGraphUrl)" + $MobileApp = Invoke-RestMethod -Uri $AppGraphUrl -Method Get -Headers $AppHeaders + if($MobileApp) + { + $MobileAppName = $MobileApp.displayName + Write-Output "Successfully found Intune Mobile App - $($MobileAppName) with MobileAppId - $($MobileAppId) in Intune" + } + else + { + Write-Output "Intune Mobile Application with mobileAppId: $($mobileAppId) not found" + } + } + catch [System.Exception] + { + Write-Output "Intune Mobile App Verify Deletion Failed with id: $($MobileAppId)" + Write-Error $_.Exception.Message + } +} + +function Verify-IntuneDeviceConfigurationProfile { + param( + $DeviceConfigProfileId + ) + try + { + $DeviceConfigProfileClientId = "" #Enter your Client Id for getting access token + #This Client Id must have Graph API permission: DeviceManagementConfiguration.Read.All + + Write-Output "Getting microsoft graph api access headers using client id - $($DeviceConfigProfileClientId)" + $DeviceConfigProfileHeaders = Get-MsiAuthHeaders -ResourceUri "https://graph.microsoft.com" -ClientId $DeviceConfigProfileClientId + $DeviceConfigProfileGraphUrl = "https://graph.microsoft.com/v1.0/deviceManagement/deviceConfigurations/$($DeviceConfigProfileId)" + Write-Output "Getting Device Configuration Profile using Url: $($DeviceConfigProfileGraphUrl)" + $DeviceConfigProfile = Invoke-RestMethod -Uri $DeviceConfigProfileGraphUrl -Method Get -Headers $DeviceConfigProfileHeaders + if($DeviceConfigProfile) + { + $DeviceConfigProfileName = $DeviceConfigProfile.displayName + Write-Output "Successfully found Intune Device Configuration Profile - $($DeviceConfigProfileName) with DeviceConfigProfileId - $($DeviceConfigProfileId) in Intune" + } + else + { + Write-Output "Intune Device Configuration Profile with DeviceConfigProfileId: $($DeviceConfigProfileId) not found" + } + } + catch [System.Exception] + { + Write-Output "Intune Device Configuration Profile Verify Deletion Failed with id: $($DeviceConfigProfileId)" + Write-Error $_.Exception.Message + } +} + +function Verify-IntuneGroupPolicy { + + param( + $GroupPolicyConfigurationId + ) + try + { + $GroupPolicyClientId = "" #Enter your Client Id for getting access token + #This Client Id must have Graph API permission: DeviceManagementConfiguration.Read.All + + Write-Output "Getting microsoft graph api access headers using client id - $($GroupPolicyClientId)" + $GroupPolicyHeaders = Get-MsiAuthHeaders -ResourceUri "https://graph.microsoft.com" -ClientId $GroupPolicyClientId + $GroupPolicyConfigurationUrl = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations/$($GroupPolicyConfigurationId)" + Write-Output "Getting Group Policy Configuration using Url: $($GroupPolicyConfigurationUrl)" + $GroupPolicy = Invoke-RestMethod -Uri $GroupPolicyConfigurationUrl -Method Get -Headers $GroupPolicyHeaders + if($GroupPolicy) + { + $GroupPolicyName = $GroupPolicy.displayName + Write-Output "Successfully fouund Group Policy Configuration - $($GroupPolicyName) with GroupPolicyConfigurationId - $($GroupPolicyConfigurationId) in Intune" + } + else + { + Write-Output "Intune Group Policy Configuration with GroupPolicyConfigurationId: $($GroupPolicyConfigurationId) not found" + } + } + catch [System.Exception] + { + Write-Output "Intune Group Policy Configuration Verify Deletion Failed with id: $($GroupPolicyConfigurationId)" + Write-Error $_.Exception.Message + } +} + +Write-Output "------------------- Starting Verify Intune Object Deletion Script -------------------" +$MobileAppId = '' #Mobile App Id to verify before deletion +$DeviceConfigurationProfileId = '' #Device Configuration Profile Id to verify before deletion +$GroupPolicyConfigurationId = '' #Group Policy Configuration Id to verify before deletion + +if($MobileAppId) +{ + Verify-IntuneMobileApp -MobileAppId $MobileAppId +} + +if($DeviceConfigurationProfileId) +{ + Verify-IntuneDeviceConfigurationProfile -DeviceConfigProfileId $DeviceConfigurationProfileId +} + +if($GroupPolicyConfigurationId) +{ + Verify-IntuneGroupPolicy -GroupPolicyConfigurationId $DeviceGroupPolicyId +} \ No newline at end of file diff --git a/VerifyDeletionAutomation/docs/VerifyDeletionAutomation.jpg b/VerifyDeletionAutomation/docs/VerifyDeletionAutomation.jpg new file mode 100644 index 0000000..b232eef Binary files /dev/null and b/VerifyDeletionAutomation/docs/VerifyDeletionAutomation.jpg differ diff --git a/VerifyDeletionAutomation/docs/VerifyDeletionAutomation.md b/VerifyDeletionAutomation/docs/VerifyDeletionAutomation.md new file mode 100644 index 0000000..9d58fdb --- /dev/null +++ b/VerifyDeletionAutomation/docs/VerifyDeletionAutomation.md @@ -0,0 +1,25 @@ +# Verify Delete Intune Automation + +Powershell script used to verify objects are in Intune. It works by getting a Graph API token using an MSI Client Id and gets the Intune object using the Graph API. + +## Prerequisites + +- To verify Intune Mobile Apps, MSI should have Graph API permission: **DeviceManagementApps.Read.All** +- To verify Intune Device Configuration Profiles or Group Policy Configurations, MSI should have Graph API permission: **DeviceManagementConfiguration.Read.All** +- Powershell ISE + +### Steps: + +1. Open PowerShell ISE in elevated mode and open the following script: Verify-DeleteIntuneObjects.ps1 +2. Manually configure the following variables for your own environment: + - MobileAppClientId: Azure MSI Client Id used to get the Graph API access token + - DeviceConfigProfileClientId: Azure MSI Client Id used to get the Graph API access token + - GroupPolicyClientId: Azure MSI Client Id used to get the Graph API access token + - MobileAppId: The Intune Mobile Application Id being verified + - DeviceConfigurationProfileId: The Intune Device Configuration Profile Id being verified + - GroupPolicyConfigurationId: The Intune Group Policy Configuration Id being verified +3. Run the Powershell script + +## Notes + +The Verify Delete Intune Automation script was originally created for use inside of Microsoft. We have modified it to be more generic, so it can be used as a template for other Intune environments outside of Microsoft.