Automating Intune installations with winget and proactive remediations

App installations can be such a chore, even when using an MDM like Intune, you have to package, configure the AAD groups, detection methods and everything else. Great for the optional apps, but what about the core applications which everyone needs? Surely there is a quicker way!

Well, after reading the Winget updates in Rudy’s blog here it got me thinking, could I use my favourite Intune tool and automate this further.

May I introduce you to my latest scripts to deploy (and remove) applications via Proactive Remediations from a central text file

As usual, the scripts can all be found on my github here

To start off, we need a couple of basic text files to include a list of applications to install and uninstall. To find the app names, you can either use “winget search appname” or have a browse of this website

The important thing to note is the version number at the top, this is what we’ll use in the detection script to see if there have been any updates:

Version 1.0
WinMerge.WinMerge
SumatraPDF.SumatraPDF

I’m just storing my two text files in Github, but anywhere will work as long as it is accessible via the internet (and it’s not like it’s anything secret)

Detection Script

Now for the detection scripts, the install and uninstall are basically the same so I’m just concentrating on the install ones here:

First download the file

$installuri = "https://github.com/andrew-s-taylor/winget/raw/main/install-apps.txt"


##Create a folder to store the lists
$AppList = "C:\ProgramData\AppList"
If (Test-Path $AppList) {
    Write-Output "$AppList exists. Skipping."
}
Else {
    Write-Output "The folder '$AppList' doesn't exist. This folder will be used for storing logs created after the script runs. Creating now."
    Start-Sleep 1
    New-Item -Path "$AppList" -ItemType Directory
    Write-Output "The folder $AppList was successfully created."
}

$templateFilePath = "C:\ProgramData\AppList\install-apps.txt"


##Download the list
Invoke-WebRequest `
-Uri $installuri `
-OutFile $templateFilePath `
-UseBasicParsing `
-Headers @{"Cache-Control"="no-cache"}

Then, at the end of the remediation, we’re renaming the file to include “old” to keep things neat. So let’s see if that exists, or if this is the first run:

$oldpath = "C:\ProgramData\AppList\install-apps-old.txt"
If (Test-Path $oldpath) {

If it doesn’t, we need to crash out to trigger remediation (and delete the file to stop any issues in the future:

    remove-item -path $templateFilePath -force
    Write-Warning "Not Compliant"
    Exit 1

If the old file exists, it’s been run before so we need to compare the version numbers and see if they differ:

$newcontent = get-content $templateFilePath | select-object -first 1
$oldcontent = get-content $oldpath | select-object -first 1
If ($newcontent -eq $oldcontent) {
    remove-item -path $templateFilePath -force
    Write-Output "Compliant"
    exit 0
}
else {
    remove-item -path $templateFilePath -force
    Write-Warning "Not Compliant"
    Exit 1

}

To put it all together:

$installuri = "https://github.com/andrew-s-taylor/winget/raw/main/install-apps.txt"


##Create a folder to store the lists
$AppList = "C:\ProgramData\AppList"
If (Test-Path $AppList) {
    Write-Output "$AppList exists. Skipping."
}
Else {
    Write-Output "The folder '$AppList' doesn't exist. This folder will be used for storing logs created after the script runs. Creating now."
    Start-Sleep 1
    New-Item -Path "$AppList" -ItemType Directory
    Write-Output "The folder $AppList was successfully created."
}

$templateFilePath = "C:\ProgramData\AppList\install-apps.txt"


##Download the list
Invoke-WebRequest `
-Uri $installuri `
-OutFile $templateFilePath `
-UseBasicParsing `
-Headers @{"Cache-Control"="no-cache"}



$oldpath = "C:\ProgramData\AppList\install-apps-old.txt"
If (Test-Path $oldpath) {
$newcontent = get-content $templateFilePath | select-object -first 1
$oldcontent = get-content $oldpath | select-object -first 1
If ($newcontent -eq $oldcontent) {
    remove-item -path $templateFilePath -force
    Write-Output "Compliant"
    exit 0
}
else {
    remove-item -path $templateFilePath -force
    Write-Warning "Not Compliant"
    Exit 1

}


}
else {
    remove-item -path $templateFilePath -force
    Write-Warning "Not Compliant"
    Exit 1
}

Remediation Script

Now, if it is a first run, or the central store has been updated, we now need to remediate and install the apps on the list (it will ignore any already present and updated)

Again, we need to download the latest list:

$installuri = "https://github.com/andrew-s-taylor/winget/raw/main/install-apps.txt"


##Create a folder to store the lists
$AppList = "C:\ProgramData\AppList"
If (Test-Path $AppList) {
    Write-Output "$AppList exists. Skipping."
}
Else {
    Write-Output "The folder '$AppList' doesn't exist. This folder will be used for storing logs created after the script runs. Creating now."
    Start-Sleep 1
    New-Item -Path "$AppList" -ItemType Directory
    Write-Output "The folder $AppList was successfully created."
}

$templateFilePath = "C:\ProgramData\AppList\install-apps.txt"


##Download the list
Invoke-WebRequest `
-Uri $installuri `
-OutFile $templateFilePath `
-UseBasicParsing `
-Headers @{"Cache-Control"="no-cache"}

Now we need to locate the Winget executable:

##Find Winget Path

$ResolveWingetPath = Resolve-Path "C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_*_x64__8wekyb3d8bbwe"
    if ($ResolveWingetPath){
           $WingetPath = $ResolveWingetPath[-1].Path
    }

$config

##Navigate to the Winget Path
cd $wingetpath

Now loop through and install each app (ignore the first line with the version):

##Loop through app list
$apps = get-content $templateFilePath | select-object -skip 1

##Install each app
foreach ($app in $apps) {

write-host "Installing $app"
.\winget.exe install --exact --id $app --silent --accept-package-agreements --accept-source-agreements
}

And finally, rename the file to the old name (deleting any already there) to free up the file for the next run:

##Delete the .old file to replace it with the new one
$oldpath = "C:\ProgramData\AppList\install-apps-old.txt"
If (Test-Path $oldpath) {
    remove-item $oldpath -Force
}

##Rename new to old
rename-item $templateFilePath $oldpath

The full code:

$installuri = "https://github.com/andrew-s-taylor/winget/raw/main/install-apps.txt"


##Create a folder to store the lists
$AppList = "C:\ProgramData\AppList"
If (Test-Path $AppList) {
    Write-Output "$AppList exists. Skipping."
}
Else {
    Write-Output "The folder '$AppList' doesn't exist. This folder will be used for storing logs created after the script runs. Creating now."
    Start-Sleep 1
    New-Item -Path "$AppList" -ItemType Directory
    Write-Output "The folder $AppList was successfully created."
}

$templateFilePath = "C:\ProgramData\AppList\install-apps.txt"


##Download the list
Invoke-WebRequest `
-Uri $installuri `
-OutFile $templateFilePath `
-UseBasicParsing `
-Headers @{"Cache-Control"="no-cache"}


##Find Winget Path

$ResolveWingetPath = Resolve-Path "C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_*_x64__8wekyb3d8bbwe"
    if ($ResolveWingetPath){
           $WingetPath = $ResolveWingetPath[-1].Path
    }

$config

##Navigate to the Winget Path
cd $wingetpath

##Loop through app list
$apps = get-content $templateFilePath | select-object -skip 1

##Install each app
foreach ($app in $apps) {

write-host "Installing $app"
.\winget.exe install --exact --id $app --silent --accept-package-agreements --accept-source-agreements
}

##Delete the .old file to replace it with the new one
$oldpath = "C:\ProgramData\AppList\install-apps-old.txt"
If (Test-Path $oldpath) {
    remove-item $oldpath -Force
}

##Rename new to old
rename-item $templateFilePath $oldpath

Deployment

One thing to note, make sure the deployment is running in the 64-bit context or it will struggle with the Winget.exe location as we are hardcoding the x64 version:

Enjoy!

Posted in Intune