So I’ve been doubting if I should make this blog. I found that others had already done this and maybe my method would just be redundant. After some slight convincing I figured my method does have its merits. One of them being that it uses the secure application model, and thus its easy to apply to all partner tenants for a CSP, the other benefit is that this could run headless as a completely automated solution.
Most MSPs create a baseline of applications for their clients that’s the same across the entire stack, 7zip, Chrome, things that we believe should be installed by default. This script allows you to apply that baseline across all your tenants.
So to get start we’ll have to do a couple of things, just to make sure you have everything:
- Setup the secure app model, and collect the information you’ll need
- Grab all the installers you want to use and put them all in a folder, I use C:\intune\Applications, so 7zip would be C:\intune\aplications\7-zip
- for each application you’ll need a new app.json. To create the app.json, you can use the example below.
- You’ll need Azcopy, and IntuneWinAppUtil. The script also download it for you, but please host the files yourselves. 🙂
Filling out the JSON actually is not that hard; for most applications you’ll only need to replace the Displayname, InstallCommandLine, UninstallCommandLine, and detection rules. If you need help on all the options I’d suggest the Graph API manual.
If you are using a path, or illegal character you can escape these by adding “\” infront of it.
Example JSON
“displayName”: “CyberDrain.com 7Zip”,
“installCommandLine”: “ninite.exe /Select "7-zip" /silent /disableshortcuts”,
“uninstallCommandLine”: “ninite.exe /Select "7-zip" /silent /uninstall”,
“description”: “Ninite Pro to Install 7zip.”,
“developer”: “CyberDrain.com”,
“owner”: “Cyberdrain.com”,
“informationUrl”: “https://cyberdrain.com”,
“privacyInformationUrl”: “https://cyberdrain.com”,
“fileName”: “IntunePackage.intunewin”,
“@odata.type”: “#microsoft.graph.win32LobApp”,
“applicableArchitectures”: “x86, x64”,
“installExperience”: {
“runAsAccount”: “user”,
“deviceRestartBehavior”: “allow”,
“@odata.type”: “microsoft.graph.win32LobAppInstallExperience”
},
“detectionRules”: [
{
“@odata.type”: “#microsoft.graph.win32LobAppFileSystemDetection”,
“path”: “%programfiles%\7-zip”,
“fileOrFolderName”: “7z.exe”,
“check32BitOn64System”: false,
“detectionType”: “exists” }
],
“returncode”: [
{
“returnCode”: 0,
“type”: “success”,
“@odata.type”: “#microsoft.graph.win32LobAppReturnCode”
},
{
“returnCode”: 1707,
“type”: “Success”,
“@odata.type”: “#microsoft.graph.win32LobAppReturnCode”
},
{
“returnCode”: 1641,
“type”: “hardReboot”,
“@odata.type”: “#microsoft.graph.win32LobAppReturnCode”
},
{
“returnCode”: 1618,
“type”: “retry”,
“@odata.type”: “#microsoft.graph.win32LobAppReturnCode”
},
{
“returnCode”: 3010,
“type”: “softReboot”,
“@odata.type”: “#microsoft.graph.win32LobAppReturnCode”
}
],
“minimumNumberOfProcessors”: “1”,
“minimumFreeDiskSpaceInMB”: “8”,
“minimumCpuSpeedInMHz”: “4”,
“minimumSupportedOperatingSystem”: {
“@odata.type”: “microsoft.graph.windowsMinimumOperatingSystem”,
“v10_1607”: true
},
“notes”: “Loaded via cyberdrain.com application script”,
“minimumMemoryInMB”: “1”
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
### The script: Deploy Intune Applications.
So this script took some figuring out, I’ve been using the examples found [here](https://github.com/microsoftgraph/powershell-intune-samples/tree/master/LOB_Application), and Ben Reader’s version right [here.](https://github.com/tabs-not-spaces/Intune-App-Deploy/blob/master/tasks/Deploy.Functions.ps1) There’s some tricks we apply but the one you should be aware of is the padding – We pad the file with a 10mb file to make sure that we can upload using Azcopy. The script is completely headless, so just run it and it will upload all the apps.
The options are straight forward – just fill in all the information and run the script. You can even re-upload apps by changing $ContinueonExistingApp to true. The script currently runs for just the tenant you specify. That way, you can schedulde multiple scripts with different options for different clients. If you’d like a version for all tenants at the same time, let me know!
```powershell
########################## Secure App Model Settings ############################
$ApplicationId = 'YourAppID'
$ApplicationSecret = 'YourAppSecret' | Convertto-SecureString -AsPlainText -Force
$TenantID = 'YourCSPTenantID'
$RefreshToken = 'yourverylongrefeshtoken'
$upn = 'UPN-Used-To-Generate-Tokens'
$CustomerTenantID = "YourCustomerTenant.onmicrosoft.com"
########################## Script Settings ############################
$ApplicationFolder = "C:\intune\Applications"
$Baseuri = "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps"
$AzCopyUri = "https://cyberdrain.com/wp-content/uploads/2020/04/azcopy.exe"
$IntuneWinAppUri = "https://cyberdrain.com/wp-content/uploads/2020/04/IntuneWinAppUtil.exe"
$ContinueOnExistingApp = $false
###################################################################
write-host "Checking AZCopy prerequisites and downloading these if required" -ForegroundColor Green
try {
$AzCopyDownloadLocation = Test-Path "$ApplicationFolder\AzCopy.exe"
if (!$AzCopyDownloadLocation) {
Invoke-WebRequest -UseBasicParsing -Uri $AzCopyUri -OutFile "$($ApplicationFolder)\AzCopy.exe"
}
}
catch {
write-host "The download and extraction of AzCopy failed. The script will stop. Error: $($_.Exception.Message)"
exit 1
}
write-host "Checking IntuneWinAppUtil prerequisites and downloading these if required" -ForegroundColor Green
try {
$AzCopyDownloadLocation = Test-Path "$ApplicationFolder\IntuneWinAppUtil.exe"
if (!$AzCopyDownloadLocation) { Invoke-WebRequest -UseBasicParsing -Uri $IntuneWinAppUri -OutFile "$($ApplicationFolder)\IntuneWinAppUtil.exe" }
}
catch {
write-host "The download and extraction of IntuneWinApp failed. The script will stop. Error: $($_.Exception.Message)"
exit 1
}
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
write-host "Generating token to log into Intune" -ForegroundColor Green
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $CustomerTenantID
$Header = @{
Authorization = "Bearer $($graphToken.AccessToken)"
}
$AppFolders = Get-ChildItem $ApplicationFolder -Directory
foreach ($App in $AppFolders) {
$intuneBody = get-content "$($app.fullname)\app.json"
$Settings = $intuneBody | ConvertFrom-Json
write-host "Creating if intune package for $($app.name) does not exists." -ForegroundColor Green
$ApplicationList = (Invoke-RestMethod -Uri $baseuri -Headers $Header -Method get -ContentType "application/json").value | where-object { $_.DisplayName -eq $settings.displayName }
if ($ApplicationList.count -gt 1 -and $ContinueOnExistingApp -eq $false) {
write-host "$($app.name) exists. Skipping this application." -ForegroundColor yellow
continue
}
write-host "Creating intune package for $($App.Name)" -ForegroundColor Green
$bytes = 10MB
[System.Security.Cryptography.RNGCryptoServiceProvider] $rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
$rndbytes = New-Object byte[] $bytes
$rng.GetBytes($rndbytes)
[System.IO.File]::WriteAllBytes("$($App.fullname)\dummy.dat", $rndbytes)
$FileToExecute = $Settings.installCommandLine.split(" ")[0]
start-process "$applicationfolder\IntuneWinAppUtil.exe" -argumentlist "-c $($App.FullName) -s $FileToExecute -o $($App.FullName)" -wait
write-host "Creating Application on intune platform for $($App.Name)" -ForegroundColor Green
$InTuneProfileURI = "$($BaseURI)"
$NewApp = Invoke-RestMethod -Uri $InTuneProfileURI -Headers $Header -body $intuneBody -Method POST -ContentType "application/json"
write-host "Getting encryption information for intune file for $($App.Name)" -ForegroundColor Green
$intuneWin = get-childitem $App.fullname -Filter *.intunewin
#unzip the detection.xml file to get manifest info and encryptioninfo.
$Directory = [System.IO.Path]::GetDirectoryName("$($intuneWin.fullname)")
Add-Type -Assembly System.IO.Compression.FileSystem
$zip = [IO.Compression.ZipFile]::OpenRead("$($intuneWin.fullname)")
$zip.Entries | Where-Object { $_.Name -like "Detection.xml" } | ForEach-Object {
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, "$Directory\Detection.xml", $true)
}
$zip.Dispose()
[xml ]$intunexml = get-content "$Directory\Detection.xml"
remove-item "$Directory\Detection.xml" -Force
#Unzip the encrypted file to prepare for upload.
$Directory = [System.IO.Path]::GetDirectoryName("$($intuneWin.fullname)")
Add-Type -Assembly System.IO.Compression.FileSystem
$zip = [IO.Compression.ZipFile]::OpenRead("$($intuneWin.fullname)")
$zip.Entries | Where-Object { $_.Name -like "IntunePackage.intunewin" } | ForEach-Object {
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, "$Directory\IntunePackage.intunewin", $true)
}
$zip.Dispose()
$ExtactedEncFile = (Get-Item "$Directory\IntunePackage.intunewin")
$intunewinFileSize = (Get-Item "$Directory\IntunePackage.intunewin").Length
$ContentBody = ConvertTo-Json @{
name = $intunexml.ApplicationInfo.FileName
size = [int64]$intunexml.ApplicationInfo.UnencryptedContentSize
sizeEncrypted = [int64]$intunewinFileSize
}
write-host "Uploading content information for $($App.Name)." -ForegroundColor Green
$ContentURI = "$($BaseURI)/$($NewApp.id)/microsoft.graph.win32lobapp/contentVersions/1/files/"
$ContentReq = Invoke-RestMethod -Uri $ContentURI -Headers $Header -body $ContentBody -Method POST -ContentType "application/json"
write-host "Trying to get file uri for $($App.Name)." -ForegroundColor Green
do {
write-host "Still trying to get file uri for $($App.Name) Please wait." -ForegroundColor Green
$AzFileUriCheck = "$($BaseURI)/$($NewApp.id)/microsoft.graph.win32lobapp/contentVersions/1/files/$($ContentReq.id)"
$AzFileUri = Invoke-RestMethod -Uri $AzFileUriCheck -Headers $Header -Method get -ContentType "application/json"
if ($AZfileuri.uploadState -like "*fail*") { break }
start-sleep 5
} while ($AzFileUri.AzureStorageUri -eq $null)
write-host "Retrieved upload URL. Uploading package $($App.Name) via AzCopy." -ForegroundColor Green
$UploadResults = & "$($ApplicationFolder)\azCopy.exe" cp "$($ExtactedEncFile.fullname)" "$($Azfileuri.AzureStorageUri)" --block-size-mb 4 --output-type 'json'
remove-item @($intunewin.fullname, $ExtactedEncFile) -Force
start-sleep 2
write-host "File uploaded. Commiting $($App.Name) with Encryption Info" -ForegroundColor Green
$EncBody = @{
fileEncryptionInfo = @{
encryptionKey = $intunexml.ApplicationInfo.EncryptionInfo.EncryptionKey
macKey = $intunexml.ApplicationInfo.EncryptionInfo.MacKey
initializationVector = $intunexml.ApplicationInfo.EncryptionInfo.InitializationVector
mac = $intunexml.ApplicationInfo.EncryptionInfo.Mac
profileIdentifier = $intunexml.ApplicationInfo.EncryptionInfo.ProfileIdentifier
fileDigest = $intunexml.ApplicationInfo.EncryptionInfo.FileDigest
fileDigestAlgorithm = $intunexml.ApplicationInfo.EncryptionInfo.FileDigestAlgorithm
}
} | ConvertTo-Json
$CommitURI = "$($BaseURI)/$($NewApp.id)/microsoft.graph.win32lobapp/contentVersions/1/files/$($ContentReq.id)/commit"
$CommitReq = Invoke-RestMethod -Uri $CommitURI -Headers $Header -body $EncBody -Method POST -ContentType "application/json"
write-host "Waiting for file commit results for $($App.Name)." -ForegroundColor Green
do {
write-host "Still trying to get commit state. Please wait." -ForegroundColor Green
$CommitStateURL = "$($BaseURI)/$($NewApp.id)/microsoft.graph.win32lobapp/contentVersions/1/files/$($ContentReq.id)"
$CommitStateReq = Invoke-RestMethod -Uri $CommitStateURL -Headers $Header -Method get -ContentType "application/json"
if ($CommitStateReq.uploadState -like "*fail*") { write-host "Commit Failed for $($App.Name). Moving on to Next application. Manual intervention will be required" -ForegroundColor red; break }
start-sleep 10
} while ($CommitStateReq.uploadState -eq "commitFilePending")
if ($CommitStateReq.uploadState -like "*fail*") { continue }
write-host "Commiting application version" -ForegroundColor Green
$ConfirmBody = @{
"@odata.type" = "#microsoft.graph.win32lobapp"
"committedContentVersion" = "1"
} | Convertto-Json
$CommitFinalizeURI = "$($BaseURI)/$($NewApp.id)"
$CommitFinalizeReq = Invoke-RestMethod -Uri $CommitFinalizeURI -Headers $Header -body $Confirmbody -Method PATCH -ContentType "application/json"
write-host "Deployment completed for app $($app.name). You can assign this app to users now." -ForegroundColor Green
}
|
So if you combine this with my earlier autopilot automation blog, you could easily setup the entire autopilot experience, with very little effort. And that’s it! as always, Happy PowerShelling.