-
Notifications
You must be signed in to change notification settings - Fork 18
/
New-Win32Package.ps1
439 lines (374 loc) · 23.3 KB
/
New-Win32Package.ps1
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
#Requires -PSEdition Desktop
#Requires -Modules Evergreen, VcRedist
<#
.SYNOPSIS
Convert an application into an Intunewin package and import into an Intune tenant.
Calls scripts/Create-Win32App.ps1 to perform the import based on App.json
.PARAMETER Path
Literal path to the packages directory within the downloaded project.
.PARAMETER PackageManifest
The package manifest file name stored in each package directory.
.PARAMETER InstallScript
Path to the template install script. If the package is configured to use Install.json, the script will be copied into the package.
.PARAMETER PSAppDeployToolkit
Path to the PSAppDeployToolkit. If a package is configured to use the PSAppDeployToolkit it will be copied into the package.
.PARAMETER Application
An array of application names to import into the target Intune tenant. The application names must match those applications stored in the project.
.PARAMETER Type
The package type to import into the target Intune tenant - App or Update. The array passed to Applications must match those application packages defined for this type.
.PARAMETER WorkingPath
Path to a working directory used when creating the Intunewin packages.
.PARAMETER Import
Switch parameter to specify that the the package should be imported into the Microsoft Intune tenant.
.PARAMETER Force
Create the package, even if a matching version already exists.
.PARAMETER Certificate
Specifies the certificate that will be used to sign the script or file. Enter a variable that stores an object representing the certificate. Used by Set-AuthenticodeSignature.
.PARAMETER CertificateSubject
Specifies the certificate subject name that will be used to sign scripts. Used by Set-AuthenticodeSignature.
.PARAMETER CertificateThumbprint
Specifies the certificate thumbprint that will be used to sign scripts. Used by Set-AuthenticodeSignature.
.PARAMETER TimestampServer
Uses the specified time stamp server to add a time stamp to the signature. Type the URL of the time stamp server as a string. The URL must start with https:// or http://. Used by Set-AuthenticodeSignature.
.PARAMETER IncludeChain
Determines which certificates in the certificate trust chain are included in the digital signature. NotRoot is the default. Used by Set-AuthenticodeSignature.
.EXAMPLE
$params = @{
Path = "E:\projects\packagefactory\packages"
Application = "AdobeAcrobatReaderDCMUI"
Type = "App"
WorkingPath = "E:\projects\packagefactory\output"
Import = $true
}
.\New-Win32Package.ps1 @params
.NOTES
Author: Aaron Parker
Twitter: @stealthpuppy
#>
[CmdletBinding()]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "", Justification = "Needed to execute Evergreen or VcRedist commands")]
param (
[Parameter(Mandatory = $false, HelpMessage = "The package name to create and import into Intune.")]
[System.String[]] $Application = @(
"AdobeAcrobatReaderDCMUI",
"ImageCustomise",
"Microsoft.NETCurrent",
"MicrosoftEdgeWebView2",
"MicrosoftVcRedist2022x86",
"MicrosoftVcRedist2022x64",
"PaintDotNetOfflineInstaller",
"VideoLanVlcPlayer",
"ZoomMeetings"),
[Parameter(Mandatory = $false, HelpMessage = "The type of package to create.")]
[ValidateSet("App", "Update")]
[System.String] $Type = "App",
[Parameter(Mandatory = $false, HelpMessage = "The path to the packages in the package factory.")]
[System.String] $Path = "E:\projects\packagefactory\packages",
[Parameter(Mandatory = $false, HelpMessage = "The manifest file that defines each package properties. Defaults to 'App.json'.")]
[System.String] $PackageManifest = "App.json",
[Parameter(Mandatory = $false, HelpMessage = "The path to the project's Install.ps1 file. This file reads 'Install.json' in the package source to perform an application install.")]
[System.String] $InstallScript = $([System.IO.Path]::Combine($PSScriptRoot, "Install.ps1")),
[Parameter(Mandatory = $false, HelpMessage = "The path to the PSAppDeployToolkit. Used if the package supports the PSAppDeployToolkit for install.")]
[System.String] $PSAppDeployToolkit = $([System.IO.Path]::Combine($PSScriptRoot, "PSAppDeployToolkit", "Toolkit")),
[Parameter(Mandatory = $false, HelpMessage = "The path to the working directory to be used when creating packages.")]
[System.String] $WorkingPath = $([System.IO.Path]::Combine($PSScriptRoot, "output")),
[Parameter(Mandatory = $false, HelpMessage = "Import the package into Microsoft Intune")]
[System.Management.Automation.SwitchParameter] $Import,
[Parameter(Mandatory = $false, HelpMessage = "Create the package, even if a matching version already exists.")]
[System.Management.Automation.SwitchParameter] $Force,
[Parameter(Mandatory = $false, HelpMessage = "Specifies the certificate that will be used to sign the script or file. Enter a variable that stores an object representing the certificate.")]
[System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate,
[Parameter(Mandatory = $false, HelpMessage = "Specifies the certificate subject name that will be used to sign scripts.")]
[System.String] $CertificateSubject,
[Parameter(Mandatory = $false, HelpMessage = "Specifies the certificate thumbprint that will be used to sign scripts.")]
[System.String] $CertificateThumbprint,
[Parameter(Mandatory = $false, HelpMessage = "Uses the specified time stamp server to add a time stamp to the signature. Type the URL of the time stamp server as a string. The URL must start with https:// or http://.")]
[System.String] $TimestampServer,
[Parameter(Mandatory = $false, HelpMessage = "Determines which certificates in the certificate trust chain are included in the digital signature. NotRoot is the default.")]
[ValidateSet("All", "Signer", "NotRoot")]
[System.String] $IncludeChain = "NotRoot"
)
begin {
#region Call functions
$ModuleFile = $(Join-Path -Path $PSScriptRoot -ChildPath "New-Win32Package.psm1")
if (Test-Path -Path $ModuleFile -PathType "Leaf" -ErrorAction "Stop") {
Import-Module -Name $ModuleFile -Force -ErrorAction "Stop"
Write-Msg -Msg "Importing module: '$ModuleFile'"
}
else {
throw [System.IO.FileNotFoundException]::New("Module file not found: '$ModuleFile'")
}
#endregion
# Set information output
$ProgressPreference = "SilentlyContinue"
$InformationPreference = "Continue"
$VerbosePreference = "Continue"
}
process {
foreach ($ApplicationName in $Application) {
# Build variables
Write-Msg -Msg "Process application: '$ApplicationName'"
$AppPath = [System.IO.Path]::Combine($Path, $Type, $ApplicationName)
$ManifestFile = $([System.IO.Path]::Combine($AppPath, $PackageManifest))
$ManifestContent = Get-Content -Path $([System.IO.Path]::Combine($AppPath, $PackageManifest))
$SourcePath = [System.IO.Path]::Combine($WorkingPath, $ApplicationName, "Source")
$OutputPath = [System.IO.Path]::Combine($WorkingPath, $ApplicationName, "Output")
Write-Msg -Msg "AppPath: '$AppPath'"
Write-Msg -Msg "ManifestFile: '$ManifestFile'"
Write-Msg -Msg "SourcePath: '$SourcePath'"
Write-Msg -Msg "OutputPath: '$OutputPath'"
# Check that the application package definition exists
Write-Msg -Msg "Check for path '$ManifestFile'"
if (Test-Path -Path $ManifestFile -PathType "Leaf") {
# Get the application details
Write-Msg -Msg "Read manifest: '$([System.IO.Path]::Combine($Path, $Type, $ApplicationName, $PackageManifest))'"
$Manifest = $ManifestContent | ConvertFrom-Json -ErrorAction "Stop"
Write-Msg -Msg "Manifest OK"
# Lets see if this application is already in Intune and needs to be updated
Write-Msg -Msg "Check for existing application in Intune"
$UpdateApp = Test-IntuneWin32App -Manifest $Manifest
# Create the package and import the application
if ($UpdateApp -eq $true -or $Force -eq $true) {
Write-Msg -Msg "Importing application: '$ApplicationName'"
# Create the target directories
Write-Msg -Msg "Create path: '$SourcePath'"
New-Item -Path $SourcePath -ItemType "Directory" -Force -ErrorAction "SilentlyContinue" | Out-Null
Write-Msg -Msg "Create path: '$OutputPath'"
New-Item -Path $OutputPath -ItemType "Directory" -Force -ErrorAction "SilentlyContinue" | Out-Null
# Remove any existing intunewin packages
Get-ChildItem -Path $OutputPath -Recurse -Include "*.intunewin" -ErrorAction "SilentlyContinue" | ForEach-Object {
Write-Msg -Msg "Removing intunewin package: '$($_.FullName)'"
Remove-Item -Path $_.FullName -Force
}
# Download the application installer
if ([System.String]::IsNullOrEmpty($Manifest.Application.Filter)) {
Write-Warning -Message "$ApplicationName not supported for automatic download"
Write-Msg -Msg "Please ensure application binaries are saved to: '$SourcePath'"
}
else {
# Check if there are files in the source folder
if ((Get-ChildItem -Path $SourcePath -Recurse -File).Count -gt 0) {
Write-Warning -Message "'$SourcePath' is not empty. Package may included extra files"
}
#region Configure the installer script logic using Install.ps1 or PSAppDeployToolkit
if (Test-Path -Path $([System.IO.Path]::Combine($AppPath, "Source", "Deploy-Application.ps1"))) {
Write-Msg -Msg "This application uses the PSAppDeployToolkit"
# Copy the PSAppDeployToolkit into the target path
$params = @{
Path = "$PSAppDeployToolkit\*"
Destination = $SourcePath
Recurse = $true
Exclude = "Deploy-Application.ps1"
ErrorAction = "Stop"
}
Write-Msg -Msg "Copy '$PSAppDeployToolkit' to '$SourcePath'"
Copy-Item @params
$params = @{
Path = $([System.IO.Path]::Combine($AppPath, "Source", "Deploy-Application.ps1"))
Destination = $([System.IO.Path]::Combine($SourcePath, "Deploy-Application.ps1"))
ErrorAction = "Stop"
}
Write-Msg -Msg "Copy deploy script: '$([System.IO.Path]::Combine($AppPath, "Source", "Deploy-Application.ps1"))' to '$([System.IO.Path]::Combine($SourcePath, "Deploy-Application.ps1"))'"
Copy-Item @params
Write-Msg -Msg "New path: '$([System.IO.Path]::Combine($SourcePath, "Files"))'"
New-Item -Path $([System.IO.Path]::Combine($SourcePath, "Files")) -ItemType "Directory" -ErrorAction "SilentlyContinue" | Out-Null
Write-Msg -Msg "New path: '$([System.IO.Path]::Combine($SourcePath, "SupportFiles"))'"
New-Item -Path $([System.IO.Path]::Combine($SourcePath, "SupportFiles")) -ItemType "Directory" -ErrorAction "SilentlyContinue" | Out-Null
# Update SourcePath to point to the PSAppDeployToolkit\Files directory
$SourcePath = [System.IO.Path]::Combine($SourcePath, "Files")
}
elseif (Test-Path -Path $([System.IO.Path]::Combine($AppPath, "Source", "Install.json"))) {
Write-Msg -Msg "This application uses Install.ps1"
# Copy the custom Install.ps1 into the target path
$Destination = $([System.IO.Path]::Combine($SourcePath, "Install.ps1"))
$params = @{
Path = $InstallScript
Destination = $Destination
ErrorAction = "Stop"
}
Write-Msg -Msg "Copy install script: '$InstallScript' to '$Destination'"
Copy-Item @params
}
else {
Write-Msg -Msg "Install.json does not exist or PSAppDeployToolkit not used"
}
#endregion
# Copy the contents of the source directory from the package definition to the working directory
$params = @{
Path = "$([System.IO.Path]::Combine($AppPath, "Source"))\*"
Destination = $SourcePath
Recurse = $true
Exclude = "Deploy-Application.ps1"
Force = $true
ErrorAction = "Stop"
}
Write-Msg -Msg "Copy: '$([System.IO.Path]::Combine($AppPath, "Source"))' to '$SourcePath'"
Copy-Item @params
# Download the application installer or run command in .Filter
Write-Msg -Msg "Invoke filter: '$($Manifest.Application.Filter)'"
if ($Manifest.Application.Filter -match "Get-EvergreenAppFromApi|Get-EvergreenApp") {
# Evergreen
Write-Msg -Msg "Downloading with Evergreen to: '$SourcePath'"
Write-Msg -Msg "Invoke: '$($Manifest.Application.Filter)'"
$EvergreenApp = Invoke-Expression -Command $Manifest.Application.Filter
Write-Msg -Msg "Found version: $($EvergreenApp.Version)"
Write-Msg -Msg "Found URL: $($EvergreenApp.URI)"
Write-Msg -Msg "Save to: '$SourcePath'"
$Result = $EvergreenApp | Save-EvergreenApp -LiteralPath $SourcePath
}
elseif ($Manifest.Application.Filter -match "Get-VcList") {
# VcRedist
Write-Msg -Msg "Downloading with VcRedist to: '$SourcePath'"
Write-Msg -Msg "Invoke: '$($Manifest.Application.Filter)'"
$EvergreenApp = Invoke-Expression -Command $Manifest.Application.Filter
Write-Msg -Msg "Found version: $($EvergreenApp.Version)"
Write-Msg -Msg "Found URL: $($EvergreenApp.URI)"
Write-Msg -Msg "Save to: '$SourcePath'"
$Result = $EvergreenApp | Save-EvergreenApp -LiteralPath $SourcePath
}
else {
# Other
Write-Msg -Msg "Executing command: '$($Manifest.Application.Filter)'"
Invoke-Expression -Command $Manifest.Application.Filter
}
#region Unpack the installer file if its a zip file
foreach ($File in $Result) { Write-Msg -Msg "Downloaded: '$($File.FullName)'" }
if ($Result.FullName -match "\.zip$") {
$params = @{
Path = $Result.FullName
DestinationPath = $SourcePath
ErrorAction = "Stop"
}
Write-Msg -Msg "Expand: '$($Result.FullName)'"
Expand-Archive @params
Write-Msg -Msg "Delete: '$($Result.FullName)'"
Remove-Item -Path $Result.FullName -Force
}
#endregion
#region Run the command defined in PrePackageCmd
if ($Manifest.Application.PrePackageCmd.Length -gt 0) {
$TempPath = $([System.IO.Path]::Combine($Env:Temp, $Result.BaseName))
$params = @{
FilePath = $Result.FullName
ArgumentList = $($Manifest.Application.PrePackageCmd -replace "#Path", $TempPath)
NoNewWindow = $true
Wait = $true
ErrorAction = "Stop"
}
Write-Msg -Msg "Start: '$($Result.FullName) $($Manifest.Application.PrePackageCmd -replace "#Path", $TempPath)'"
Start-Process @params
$params = @{
Path = "$TempPath\*"
Destination = $SourcePath
Recurse = $true
Force = $true
ErrorAction = "Stop"
}
Write-Msg -Msg "Copy from: '$TempPath', to: '$SourcePath'"
Copy-Item @params
Write-Msg -Msg "Delete: '$($Result.FullName)'"
Remove-Item -Path $Result.FullName -Force
}
#endregion
}
# Check for the valid MSI - if changed uninstallation and detection will fail
if ($Manifest.PackageInformation.SetupType -eq "MSI") {
# Get Real GUID from .msi File
Write-Msg -Msg "Get MSI GUID from: '$([System.IO.Path]::Combine($SourcePath, $($Manifest.PackageInformation.SetupFile)))'"
$MsiID = Get-MsiProductCode -Path $([System.IO.Path]::Combine($SourcePath, $($Manifest.PackageInformation.SetupFile)))
$MsiGuid = [System.Guid]::New($MsiID)
Write-Msg -Msg "MSI GUID: '$($MsiGuid.GUID)'"
# Check the GUID in the uninstall string
if ($Manifest.Program.UninstallCommand -match "{\w{8}-\w{4}-\w{4}-\w{4}-\w{12}}") {
$UninstallGuid = [System.Guid]::New($Matches[0])
if (-not($UninstallGuid.Equals($MsiGuid))) {
Write-Warning -Message "Uninstall string '$($UninstallGuid.GUID)' does not match MSI package ID: '$($MsiID.GUID)'"
}
}
# Check the GUID in the detection rules
foreach ($Rule in ($Manifest.DetectionRule | Where-Object { $_.Type -eq "Registry" })) {
if ($Rule.KeyPath -match "{\w{8}-\w{4}-\w{4}-\w{4}-\w{12}}") {
$DetectionGuid = [System.Guid]::New($Matches[0])
if (-not($DetectionGuid.Equals($MsiGuid))) {
Write-Warning -Message "Detection rule registry path '$($Rule.KeyPath)' does not match MSI package ID: '$($MsiID.GUID)'"
}
}
}
}
# Sign the scripts in the package
if ($PSBoundParameters.ContainsKey("Certificate") -or $PSBoundParameters.ContainsKey("CertificateSubject") -or $PSBoundParameters.ContainsKey("CertificateThumbprint")) {
if ($PSBoundParameters.ContainsKey("Certificate")) {
$params = @{
Path = $SourcePath
Certificate = $Certificate
}
}
elseif ($PSBoundParameters.ContainsKey("CertificateSubject")) {
$params = @{
Path = $SourcePath
CertificateSubject = $CertificateSubject
}
}
elseif ($PSBoundParameters.ContainsKey("CertificateThumbprint")) {
$params = @{
Path = $SourcePath
CertificateThumbprint = $CertificateThumbprint
}
}
Write-Msg -Msg "Signing scripts in '$SourcePath'"
Set-ScriptSignature @params | ForEach-Object { Write-Msg -Msg "Signed script: $($_.Path)" }
}
#region Create the intunewin package
# Adjust params for New-IntuneWin32AppPackage using PSAppDeployToolkit
$IntuneWinSetupFile = $Manifest.PackageInformation.SetupFile
if (Test-Path -Path $([System.IO.Path]::Combine($AppPath, "Source", "Deploy-Application.ps1"))) {
# Revert source path
$SourcePath = [System.IO.Path]::Combine($WorkingPath, $ApplicationName, "Source")
$IntuneWinSetupFile = "Deploy-Application.exe"
}
# Create the intunewin package
if ($Result.FullName -match "\.intunewin$") {
Write-Msg -Msg "Copy downloaded intunewin file to: '$Path\output'"
Copy-Item -Path $Result.FullName -Destination $OutputPath -Force
}
else {
$params = @{
SourceFolder = $SourcePath
SetupFile = $IntuneWinSetupFile
OutputFolder = $OutputPath
Force = $true
}
Write-Msg -Msg "Create intunewin package in: '$OutputPath'"
Write-Msg -Msg "Source folder: '$SourcePath'"
Write-Msg -Msg "Setup file: '$IntuneWinSetupFile'"
$IntuneWinPackage = New-IntuneWin32AppPackage @params
}
# Get the package file
$PackageFile = Get-ChildItem -Path $OutputPath -Recurse -Include "*.intunewin" -ErrorAction "SilentlyContinue"
if ($null -eq $PackageFile) { throw [System.IO.FileNotFoundException]::New("Intunewin package file not found") }
if ($PackageFile.Count -gt 1) { throw [System.IO.InvalidDataException]::New("Found more than 1 intunewin file") }
Write-Msg -Msg "Found package file: '$($PackageFile.FullName)'"
#endregion
#region Import the package
if ($Import -eq $true) {
Write-Msg -Msg "-Import specified. Importing package into tenant"
# Launch script to import the package
$params = @{
Json = $([System.IO.Path]::Combine($AppPath, $PackageManifest))
PackageFile = $IntuneWinPackage.Path
}
Write-Msg -Msg "Create package with: '$PSScriptRoot\scripts\Create-Win32App.ps1'"
Write-Msg -Msg "Package manifest: '$([System.IO.Path]::Combine($AppPath, $PackageManifest))'"
Write-Msg -Msg "Package file: '$IntuneWinPackage.Path'"
& "$PSScriptRoot\scripts\Create-Win32App.ps1" @params | Select-Object -Property * -ExcludeProperty "largeIcon"
}
#endregion
}
}
else {
Write-Warning -Message "Cannot find package definition for '$ApplicationName' in '$AppPath'"
}
}
}
end {
}