asheroto / winget-install

Install winget tool using PowerShell! Prerequisites automatically installed. Works on Windows 10/11 and Server 2019/2022.
https://bit.ly/winget-install
GNU General Public License v3.0
272 stars 33 forks source link

[Bug]: SYSTEM user will not run Add-AppxPackage, winget will not install properly #40

Closed GraphicHealer closed 5 months ago

GraphicHealer commented 5 months ago

Checklist

What You Are Seeing?

I am running winget-install as the system user, and the system user is not allowed to run the Add-AppxPackage command. Also, WinGet will not run properly as the SYSTEM user after install, even if I run it directly from the C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.22.11132.0_x64__8wekyb3d8bbwe directory.

System Details

image

Additional Context

I am trying to implement this into the RMM software I use at my workplace, so we can utilize WinGet as our package management solution.

Full Output With -DebugMode

PS C:\Windows\system32> winget-install -Force -DebugMode
C:\Program Files\WindowsPowerShell\Scripts\winget-install.ps1 : A parameter cannot be found that matches parameter 
name 'DebugMode'.
At line:1 char:23
+ winget-install -Force -DebugMode
+                       ~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [winget-install.ps1], ParameterBindingException
    + FullyQualifiedErrorId : NamedParameterNotFound,winget-install.ps1

PS C:\Windows\system32> winget-install -Force -Debug
winget-install 4.0.4
To check for updates, run winget-install -CheckForUpdate

#################
# Prerequisites #
#################

Downloading VCLibs...
DEBUG: Downloading VCLibs from https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx to C:\Windows\TEMP\bnazp150.tyi

Downloading UI.Xaml...
DEBUG: Downloading UI.Xaml from 
https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.x64.appx to
C:\Windows\TEMP\uu0i0mci.3kg

##########
# winget #
##########

DEBUG: Getting latest release...
Downloading winget license...
DEBUG: Downloading winget license from 
https://github.com/microsoft/winget-cli/releases/download/v1.7.11132/ccfd1d114c9641fc8491f3c7c179829e_License1.xml to
C:\Windows\TEMP\keuzlq30.itj

Downloading winget...
DEBUG: Downloading winget from https://aka.ms/getwinget to C:\Windows\TEMP\31k4bfcf.qhd

Installing winget and its dependencies...

############
# Complete #
############

winget installed successfully.
Checking if winget is installed and working...

###############
# Registering #
###############

Add-AppxPackage : Deployment failed with HRESULT: 0x80073CF9, Install failed. Please contact your software vendor. 
(Exception from HRESULT: 0x80073CF9)
Deployment Register operation rejected on package Microsoft.DesktopAppInstaller_2024.423.111.0_neutral_~_8wekyb3d8bbwe 
from: AppxBundleManifest.xml install request because the Local System account is not allowed to perform this operation.
NOTE: For additional information, look for [ActivityId] 726e93c7-9819-0000-1185-6f721998da01 in the Event Log or use 
the command line Get-AppPackageLog -ActivityID 726e93c7-9819-0000-1185-6f721998da01
At C:\Program Files\WindowsPowerShell\Scripts\winget-install.ps1:836 char:13
+             Add-AppxPackage -RegisterByFamilyName -MainPackage Micros ...
+             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : WriteError: (Microsoft.Deskt...r_8wekyb3d8bbwe:String) [Add-AppxPackage], IOException
    + FullyQualifiedErrorId : DeploymentError,Microsoft.Windows.Appx.PackageManager.Commands.AddAppxPackageCommand

winget command registered successfully.
asheroto commented 5 months ago

Hey there. I fixed the -DebugMode in the bug report template. The script used to use that but as you found just uses -Debug.


I spun up a VM with Windows 11 Pro on it and used psexec.exe to test with the SYSTEM user account. Although winget is installed, I experienced the same issue when the "registering" step appeared.

Add-AppxPackage : Deployment failed with HRESULT: 0x80073CF9, Install failed. Please contact your software vendor. 
(Exception from HRESULT: 0x80073CF9)
Deployment Register operation rejected on package Microsoft.DesktopAppInstaller_2024.423.111.0_neutral_~_8wekyb3d8bbwe 
from: AppxBundleManifest.xml install request because the Local System account is not allowed to perform this operation.

Since winget is included in Windows 11 now, I was still able to use winget, even after running this installation. Running C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.22.11132.0_x64__8wekyb3d8bbwe\winget.exe actually worked for me.

I then repeated the test on a Windows 10 Pro VM, and had the same error. This time, though, since winget isn't included in Windows 10, I was able to run winget, but it does not output anything. That means that even if we resolve this error, winget won't run anyway. Unfortunately this appears to be a Microsoft issue.

image

I then tried installing it as an administrator user, not SYSTEM, and it worked fine. This of course, just confirms that the SYSTEM user is not supported. The "registering" step is the last step of the process, and it only runs if PowerShell can't run winget already.


Unfortunately it's not something we can change in the script to fix it. It's a Microsoft issue with winget-cli itself. winget-cli does not support the SYSTEM account. SYSTEM account usage for winget is discussed in this issue. Interestingly enough, other IT admins have mentioned that they too want to use their RMM for winget deployment but ran into similar issues when installing.

Here are the instructions from Microsoft on how to install winget from the command line. If you run these using the SYSTEM account you will find the same bug is present.

https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget

image

I am still glad you reported this, because I can improve the error handling and also detect SYSTEM accounts when used and prevent the script from running.


I'm actually a former sysadmin for an MSP, so definitely feel the frustration caused by this. In many of the RMMs I've used in the past, I was able to specify which user account to run the script under. That may be the best solution here.

GraphicHealer commented 5 months ago

Well shoot. I was hoping there may have been a workaround for this, but apparently not. I guess I'll just have to wait and see if Microsoft will ever even give the system account permission to use WinGet. I feel like they need to, considering the scope of what WinGet is supposed to do.

They would be leaving a lot of possible users and use cases on the table because of this.

Thanks for the quick response though! And the info! I guess I'll just have to wait and see what happens.

I have heard that the RMM my job uses, NinjaOne, is supposedly working on implementing WinGet into their systems for managing and updating programs automatically. I'm really hoping they can get it to work properly, as it would drastically speed up and simplify our current processes.

I am currently having to use chocolatey, and I keep having to tweak my scripts to fix issues that pop up here and there. Let's hope WinGet will get it working!

Again, thank you. G

GraphicHealer commented 5 months ago

Oh Shoot! @asheroto please dont completly block the script, just the registration step that fails.

The script still works when run as SYSTEM. WinGet is installed, SYSTEM just can't access it directly. I still would like to install WInGet with our RMM under the SYSTEM user, I will just have to run WinGet itself differently once installed. Possibly under the Local Admin user.

Thanks!

asheroto commented 5 months ago

I haven't used NinjaOne, but a lot of RMMs will let you use another account. If not, you might be able to use runas.exe or even create a scheduled task and specify the user account and run it.

Oddly enough, I just tested it again on Windows 10 and was able to run winget after all even under the SYSTEM account. So yeah, I'll leave as-is in regards to installation. Instead of blocking, I'll just add in a notice for that particular error that it may not work with the SYSTEM account. 😊

asheroto commented 5 months ago

Here's what version 4.0.5 will look like in this case.

image

asheroto commented 5 months ago

Take a look at this, not sure if would work

https://github.com/microsoft/winget-cli/discussions/962#discussioncomment-4440131

GraphicHealer commented 5 months ago

Take a look at this, not sure if would work

https://github.com/microsoft/winget-cli/discussions/962#discussioncomment-4440131

Lol! That is almost exactly what I'm doing!

I actually got it to work pretty much perfectly, I ended up writing my own script to install it for the moment.

Worked with a couple of other people in the NinjaOne community to figure it all out.

Thanks!

GraphicHealer commented 5 months ago

Actually, I'm going to put in another bug report for your program, cuz I did find one other issue that your system apparently doesn't account for.

asheroto commented 5 months ago

Hmm, so weird. So if you use an alias and find the winget exe in WindowsApps then winget works fine with the system account? Might be able to incorporate that into the script if so.

asheroto commented 5 months ago

I tried doing this, but sysget still didn't work, at least on Windows 10...

$exeFilename = Get-ChildItem -Path "C:\Program Files\WindowsApps\" -Filter winget.exe -Recurse | ForEach-Object { $_.FullName }
New-Alias -Name sysget -Value $exeFilename
sysget upgrade --all -e --accept-package-agreements --accept-source-agreements

winget is actually installed though, just not usable under the system account, only the administrator account.

How did you get it to work?

asheroto commented 5 months ago

@GraphicHealer did you only use Windows 11 or also try Windows 10?

GraphicHealer commented 5 months ago

Give me a little bit, and I can send you my script that I wrote.

asheroto commented 5 months ago

Awesome, thank you! Once you send it I'll take a look and see if I can incorporate into this script. If so, I'll give you credit.

GraphicHealer commented 5 months ago
<# Winget-Install.ps1

Source for Some Functions: https://github.com/homotechsual/ninjaget/blob/dev/PS/NinjaGet.Functions.Installation.ps1

2023-11-22 - 0.0.2 - assembled and modified by David Szpunar - Initial version

DOWNLOAD: https://discord.com/channels/676451788395642880/1232432179141808228

2024-04-23 - 0.1.0 - Simplified and Modified by GraphicHealer - Initial Version

DESCRIPTION: Installs or updates WinGet.exe to the latest version. Intended to run as SYSTEM.
        Use '-Force' to ignore version check.
-----
LICENSE: The MIT License (MIT)

Original Copyright 2023 Mikey O'Toole, modified by David Szpunar, Simplified and Modified by GraphicHealer.
#>

# Add -Force switch to force install
param(
    [switch]$Force = $false
)

# Start Logging
Start-Transcript -Path (Ninja-Property-Get DefaultLogFile) -IncludeInvocationHeader -Append -Force

# Setup Error Codes
$Script:ExitCode = 0

# Function to get OS Information
function Get-OSInfo {
    try {
        # Get registry values
        $registryValues = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
        $releaseIdValue = $registryValues.ReleaseId
        $displayVersionValue = $registryValues.DisplayVersion
        $nameValue = $registryValues.ProductName
        $editionIdValue = $registryValues.EditionId

        # Strip out "Server" from the $editionIdValue if it exists
        $editionIdValue = $editionIdValue -replace 'Server', ''

        # Get OS details using Get-CimInstance because the registry key for Name is not always correct with Windows 11
        $osDetails = Get-CimInstance -ClassName Win32_OperatingSystem
        $nameValue = $osDetails.Caption

        # Get architecture details of the OS (not the processor)
        # Get only the numbers
        $architecture = ($osDetails.OSArchitecture -replace '[^\d]').Trim()

        # If 32-bit or 64-bit replace with x32 and x64
        if ($architecture -eq '32') {
            $architecture = 'x32'
        } elseif ($architecture -eq '64') {
            $architecture = 'x64'
        }

        # Get OS version details (as version object)
        $versionValue = [System.Environment]::OSVersion.Version

        # Determine product type
        # Reference: https://learn.microsoft.com/en-us/dotnet/api/microsoft.powershell.commands.producttype?view=powershellsdk-1.1.0
        if ($osDetails.ProductType -eq 1) {
            $typeValue = 'Workstation'
        } elseif ($osDetails.ProductType -eq 2 -or $osDetails.ProductType -eq 3) {
            $typeValue = 'Server'
        } else {
            $typeValue = 'Unknown'
        }

        # Extract numerical value from Name
        $numericVersion = ($nameValue -replace '[^\d]').Trim()

        # Create and return custom object with the required properties
        $result = [PSCustomObject]@{
            ReleaseId      = $releaseIdValue
            DisplayVersion = $displayVersionValue
            Name           = $nameValue
            Type           = $typeValue
            NumericVersion = $numericVersion
            EditionId      = $editionIdValue
            Version        = $versionValue
            Architecture   = $architecture
        }

        return $result
    } catch {
        Write-Error "Unable to get OS version details.`nError: $_"
        exit 1
    }
}

# Function to Install WinGet (modified)
function Install-WinGet {
    # Install VCPPRedist Requirements
    $Visual2019 = 'Microsoft Visual C++ 2015-2019 Redistributable*'
    $Visual2022 = 'Microsoft Visual C++ 2015-2022 Redistributable*'
    $VCPPInstalled = Get-Item @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') | Where-Object {
        $_.GetValue('DisplayName') -like $Visual2019 -or $_.GetValue('DisplayName') -like $Visual2022
    }
    if ([System.Environment]::Is64BitOperatingSystem) {
        $OSArch = 'x64'
    } else {
        $OSArch = 'x86'
    }
    if (!($VCPPInstalled)) {
        Write-Output 'VCPPRedist Requirements Not Installed. Installing...'

        $VCPPRedistURL = ('https://aka.ms/vs/17/release/vc_redist.{0}.exe' -f $OSArch)
        $VCPPRedistFileName = [Uri]$VCPPRedistURL | Select-Object -ExpandProperty Segments | Select-Object -Last 1
        $WebClient = New-Object System.Net.WebClient
        $VCPPRedistDownloadPath = "$ENV:SystemRoot\Temp\VCPRedist"
        if (!(Test-Path -Path $VCPPRedistDownloadPath)) {
            $null = New-Item -Path $VCPPRedistDownloadPath -ItemType Directory -Force
        }
        $VCPPRedistDownloadFile = "$VCPPRedistDownloadPath\$VCPPRedistFileName"
        $WebClient.DownloadFile($VCPPRedistURL, $VCPPRedistDownloadFile)
        try {
            Start-Process -FilePath $VCPPRedistDownloadFile -ArgumentList '/quiet', '/norestart' -Wait -ErrorAction Stop | Out-Null
            Write-Output 'VCPPRedist Requirements Installed.'
        } catch {
            Write-Output 'Failed to install VCPPRedist Requirements!'
            $Script:ExitCode = 1
            return
        } finally {
            Remove-Item -Path $VCPPRedistDownloadPath -Recurse -Force -ErrorAction SilentlyContinue
        }
    } else {
        Write-Output 'VCPPRedist Requirements Installed.'
    }

    # Find Latest Download
    $LatestRelease = (Invoke-RestMethod -Uri 'https://api.github.com/repos/microsoft/winget-cli/releases/latest' -Method Get)
    $LatestAsset = ($LatestRelease).assets | Where-Object { $_.name.EndsWith('.msixbundle') }
    [version]$LatestVersion = $LatestRelease.tag_name.TrimStart('v')
    if ((Find-WinGet)) {
        [version]$CurrentVersion = (& (Find-WinGet) '-v').TrimStart('v')
    } else {
        [version]$CurrentVersion = 0
    }

    # Check if WinGet is Updated
    if (!($CurrentVersion -lt $LatestVersion) -and !$Force) {
        # Exit
        Write-Output 'WinGet is up to date. Exiting.'
        return
    }

    # Installing WinGet
    Write-Output 'WinGet not installed or out of date. Installing/updating...'

    # Download WinGet to Temp
    $WebClient = New-Object System.Net.WebClient
    $WinGetDownloadPath = Join-Path -Path "$ENV:SystemRoot\Temp" -ChildPath $LatestAsset.name
    Write-Output "Downloading WinGet to $WinGetDownloadPath..."
    $WebClient.DownloadFile($LatestAsset.browser_download_url, $WinGetDownloadPath)

    # Download WinGet Source to Temp
    $WinGetSourceDownloadPath = Join-Path -Path "$ENV:SystemRoot\Temp" -ChildPath 'source.msix'
    Write-Output "Downloading WinGet to $WinGetSourceDownloadPath..."
    Invoke-WebRequest -Uri 'https://cdn.winget.microsoft.com/cache/source.msix' -OutFile $WinGetSourceDownloadPath

    $WinArch = (Get-OSInfo).Architecture

    # Download VCLibs
    $VCLibs_Path = Join-Path -Path "$ENV:SystemRoot\Temp" -ChildPath "Microsoft.VCLibs.${WinArch}.14.00.Desktop.appx"
    $VCLibs_Url = "https://aka.ms/Microsoft.VCLibs.${WinArch}.14.00.Desktop.appx"
    Write-Output 'Downloading VCLibs...'
    Write-Debug "Downloading VCLibs from $VCLibs_Url to $VCLibs_Path`n`n"
    Invoke-WebRequest -Uri $VCLibs_Url -OutFile $VCLibs_Path

    # Download UI.Xaml
    $UIXaml_Path = Join-Path -Path "$ENV:SystemRoot\Temp" -ChildPath "Microsoft.UI.Xaml.2.8.${WinArch}.appx"
    $UIXaml_Url = "https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.6/Microsoft.UI.Xaml.2.8.${WinArch}.appx"
    Write-Output 'Downloading UI.Xaml...'
    Write-Debug "Downloading UI.Xaml from $UIXaml_Url to $UIXaml_Path`n"
    Invoke-WebRequest -Uri $UIXaml_Url -OutFile $UIXaml_Path

    # Install Downloaded WinGet
    try {
        Write-Output 'Installing WinGet...'
        Add-AppxProvisionedPackage -Online -PackagePath $WinGetDownloadPath -DependencyPackagePath $UIXaml_Path, $VCLibs_Path -SkipLicense -ErrorAction Stop
        Add-AppxProvisionedPackage -Online -PackagePath $WinGetSourceDownloadPath -SkipLicense -ErrorAction Stop
        Write-Output 'WinGet installed.'
    } catch {
        Write-Output 'Failed to install WinGet!'
        $Script:ExitCode = 1
    } finally {
        Remove-Item -Path $WinGetDownloadPath -Force -ErrorAction SilentlyContinue
    }

    return
}

# Function to Find the WinGet exe (Modified)
function Find-WinGet {
    # Get the WinGet path (for use when running in SYSTEM context).
    $WinGetPathToResolve = Join-Path -Path $ENV:ProgramFiles -ChildPath 'WindowsApps\Microsoft.DesktopAppInstaller_*_*__8wekyb3d8bbwe'
    $ResolveWinGetPath = Resolve-Path -Path $WinGetPathToResolve | Sort-Object {
        [version]($_.Path -replace '^[^\d]+_((\d+\.)*\d+)_.*', '$1')
    }
    if ($ResolveWinGetPath) {
        # If we have multiple versions - use the latest.
        $WinGetPath = $ResolveWinGetPath[-1].Path
    }

    # Get the User-Context WinGet exe location.
    $WinGetExePath = Get-Command -Name winget.exe -CommandType Application -ErrorAction SilentlyContinue

    # Select the correct WinGet exe
    if (Test-Path -Path (Join-Path $WinGetPath 'winget.exe')) {
        # Running in SYSTEM-Context.
        $WinGet = Join-Path $WinGetPath 'winget.exe'
    } elseif ($WinGetExePath) {
        # Get User-Context if SYSTEM-Context not found.
        $WinGet = $WinGetExePath.Path
    } else {
        Write-Output 'WinGet not Found!'
        Stop-Transcript
        exit 1
    }

    # Return WinGet Location
    return $WinGet
}

# Test internet connection
$MaxNet = 15
Write-Output 'Checking Internet Connection...'
for ($i = 0; ($i -lt $MaxNet) -and !$net; $i++) {
    $net = Test-Connection 1.1.1.1 -Count 1 -Delay 1 -Quiet
}
if (!$net) {
    Write-Output 'No Internet Detected. Cancelling Installation.'
    Stop-Transcript
    exit 1
}

Write-Output 'Internet Detected, Continuing Installation.'

# Test OS Compatibility
$osVersion = Get-OSInfo

# If it's a workstation, make sure it is Windows 10+
if ($osVersion.Type -eq 'Workstation' -and $osVersion.NumericVersion -lt 10) {
    Write-Error 'winget requires Windows 10 or later on workstations. Your version of Windows is not supported.'
    Stop-Transcript
    exit 0
}

# If it's a workstation with Windows 10, make sure it's version 1809 or greater
if ($osVersion.Type -eq 'Workstation' -and $osVersion.NumericVersion -eq 10 -and $osVersion.ReleaseId -lt 1809) {
    Write-Error 'winget requires Windows 10 version 1809 or later on workstations. Please update Windows to a compatible version.'
    Stop-Transcript
    exit 0
}

# Run the Install/Update
Install-WinGet

& (Find-WinGet) 'list' '--accept-source-agreements'

Stop-Transcript
exit $Script:ExitCode
GraphicHealer commented 5 months ago

That's my script. It has a few calls to our RMM, (Ninja-Property-Get) but other than that, it's pretty much standalone.

asheroto commented 5 months ago

Thanks, I'll check it out.