From 20104565c3dfb3ca41d9dcadd892165e23d1e4db Mon Sep 17 00:00:00 2001 From: ThioJoe <12518330+ThioJoe@users.noreply.github.com> Date: Sun, 3 Aug 2025 20:15:47 -0700 Subject: [PATCH] Initial Commit --- Installer Scripts/Install-Microsoft-Store.ps1 | 484 ++++++++++++++++++ Installer Scripts/Install-Winget.ps1 | 156 ++++++ LICENSE | 21 + README.md | 2 + Sandbox Configurations/MyDefaultSandbox.wsb | 45 ++ Sandbox Configurations/UseGoogleDNS.wsb | 9 + Startup Scripts/SandboxStartup.ps1 | 50 ++ Theme Scripts/Set Theme Dark Mode.ps1 | 28 + Theme Scripts/Set Theme Light Mode.ps1 | 10 + 9 files changed, 805 insertions(+) create mode 100644 Installer Scripts/Install-Microsoft-Store.ps1 create mode 100644 Installer Scripts/Install-Winget.ps1 create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Sandbox Configurations/MyDefaultSandbox.wsb create mode 100644 Sandbox Configurations/UseGoogleDNS.wsb create mode 100644 Startup Scripts/SandboxStartup.ps1 create mode 100644 Theme Scripts/Set Theme Dark Mode.ps1 create mode 100644 Theme Scripts/Set Theme Light Mode.ps1 diff --git a/Installer Scripts/Install-Microsoft-Store.ps1 b/Installer Scripts/Install-Microsoft-Store.ps1 new file mode 100644 index 0000000..1723049 --- /dev/null +++ b/Installer Scripts/Install-Microsoft-Store.ps1 @@ -0,0 +1,484 @@ +# This script will install the Microsoft Store into Windows Sandbox +# It uses the Windows Update API to fetch the necessary installation files directly from Microsoft +# Unlike many similar scripts, it uses NO dependencies or third party APIs + +# Author: ThioJoe +# Repo Url: https://github.com/ThioJoe/Windows-Sandbox-Tools + +# --- Configuration --- +# Category ID for the Microsoft Store app package +$storeCategoryId = "64293252-5926-453c-9494-2d4021f1c78d" + +# Flight Ring - Use "Retail" for the public version. +# Note: Values other than retail are not properly set up yet in this script +$flightRing = "Retail" + +# Other Known options: +# "RP" (ReleasePreview Branch) +# "WIS" (Beta Branch) +# "WIF" (Dev Branch) +# Other Possible Options, Untested: +# "Canary" +# "MSIT" (Internal) + + +# --- Define Working Directory --- +# Get the path to the user's personal Downloads folder in a reliable way +$userDownloadsFolder = (New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path + +# Define the subfolder name for all our files +$subfolderName = "MSStore Install" + +# Combine them to create the full working directory path +$workingDir = Join-Path -Path $userDownloadsFolder -ChildPath $subfolderName + +# Create the directory if it doesn't exist +if (-not (Test-Path -Path $workingDir)) { + New-Item -Path $workingDir -ItemType Directory -Force | Out-Null +} + +Write-Host "All files (logs, downloads) will be saved to: '$workingDir'" -ForegroundColor Yellow + +# --- XML Templates --- + +# Step 1: GetCookie request body. +# See: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wusp/36a5d99a-a3ca-439d-bcc5-7325ff6b91e2 +$cookieXmlTemplate = @" + + + http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService/GetCookie + urn:uuid:$(New-Guid) + https://fe3.delivery.mp.microsoft.com/ClientWebService/client.asmx + + + + + + + + +"@ + +# Step 2: SyncUpdates request body. Based on intercepted XML request using Microsoft Store. +# Info about attributes found here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wusp/6b654980-ae63-4b0d-9fae-2abb516af894 +$fileListXmlTemplate = @" + + + http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService/SyncUpdates + urn:uuid:$(New-Guid) + https://fe3cr.delivery.mp.microsoft.com/ClientWebService/client.asmx + + + $((Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'")) + $((Get-Date).AddMinutes(5).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'")) + + + + + + + + + + + + $((Get-Date).AddYears(10).ToUniversalTime().ToString('u').Replace(' ','T')) + {0} + + + false + + 123111923599745169044 + 87888302311099323110994543419005983000659830007 + 598300086048401062450018624500196245002098959022 + 98959023989590249895902598959026104433538129905029 + 130040031132387090132393049133399034138537048140377312 + 143747671158941041158941042158941043158941044159123858 + 159130928164836897164847386164848327164852241164852246 + 164852253 + + false + false + + + {1} + + + true + false + + + + Extended + + + en-US + en + + + + en-US + + + false + E:BranchReadinessLevel=CB&CurrentBranch=rs_prerelease&OEMModel=Virtual%20Machine&FlightRing=Retail&AttrDataVer=321&InstallLanguage=en-US&OSUILocale=en-US&InstallationType=Client&FlightingBranchName=&OSSkuId=48&App=WU_STORE&ProcessorManufacturer=GenuineIntel&OEMName_Uncleaned=Microsoft%20Corporation&AppVer=1407.2503.28012.0&OSArchitecture=AMD64&IsFlightingEnabled=1&TelemetryLevel=1&DefaultUserRegion=39070&WuClientVer=1310.2503.26012.0&OSVersion=10.0.26100.3915&DeviceFamily=Windows.Desktop + Interactive=1;IsSeeker=1; + + + + + + +"@ + +# Step 3: GetExtendedUpdateInfo2 - After getting the list of matched files (app version and dependencies), this lets us get the actual download URLs +# See: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wusp/2f66a682-164f-47ec-968e-e43c0a85dc21 +$fileUrlXmlTemplate = @" + + + http://www.microsoft.com/SoftwareDistribution/Server/ClientWebService/GetExtendedUpdateInfo2 + urn:uuid:$(New-Guid) + https://fe3cr.delivery.mp.microsoft.com/ClientWebService/client.asmx/secured + + + $((Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'")) + $((Get-Date).AddMinutes(5).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'")) + + + {0} + + + + + + {1}{2} + FileUrl + E:BranchReadinessLevel=CB&CurrentBranch=rs_prerelease&OEMModel=Virtual%20Machine&FlightRing={3}&AttrDataVer=321&InstallLanguage=en-US&OSUILocale=en-US&InstallationType=Client&FlightingBranchName=&OSSkuId=48&App=WU_STORE&ProcessorManufacturer=GenuineIntel&OEMName_Uncleaned=Microsoft%20Corporation&AppVer=1407.2503.28012.0&OSArchitecture=AMD64&IsFlightingEnabled=1&TelemetryLevel=1&DefaultUserRegion=39070&WuClientVer=1310.2503.26012.0&OSVersion=10.0.26100.3915&DeviceFamily=Windows.Desktop + + + +"@ + +# --- Script Execution --- +$headers = @{ "Content-Type" = "application/soap+xml; charset=utf-8" } +$baseUri = "https://fe3.delivery.mp.microsoft.com/ClientWebService/client.asmx" + +try { + # Step 1: Get Cookie + Write-Host "Step 1: Getting authentication cookie..." + $cookieRequestPayload = $cookieXmlTemplate + #$cookieRequestPayload | Set-Content -Path (Join-Path $LogDirectory "01_Step1_Request.xml") + + $cookieResponse = Invoke-WebRequest -Uri $baseUri -Method Post -Body $cookieRequestPayload -Headers $headers -UseBasicParsing + #$cookieResponse.Content | Set-Content -Path (Join-Path $LogDirectory "01_Step1_Response.xml") + Write-Host " -> Saved request and response logs for Step 1." + + $cookieResponseXml = [xml]$cookieResponse.Content + $encryptedCookieData = $cookieResponseXml.Envelope.Body.GetCookieResponse.GetCookieResult.EncryptedData + Write-Host "Success. Cookie received." -ForegroundColor Green + + # Step 2: Get File List + Write-Host "Step 2: Getting file list..." + $fileListRequestPayload = $fileListXmlTemplate -f $encryptedCookieData, $storeCategoryId, $flightRing + $tempRequestFile = Join-Path $workingDir "02_Step2_Request_AUTOMATED.xml" + [System.IO.File]::WriteAllText($tempRequestFile, $fileListRequestPayload, [System.Text.UTF8Encoding]::new($false)) + + $fileListResponse = Invoke-WebRequest -Uri $baseUri -Method Post -InFile $tempRequestFile -Headers $headers -UseBasicParsing + #$fileListResponse.Content | Set-Content -Path (Join-Path $LogDirectory "02_Step2_Response_SUCCESS.xml") + Write-Host " -> Saved request and response logs for Step 2." + + # The response contains XML fragments that are HTML-encoded. We must decode this before treating it as XML. + Add-Type -AssemblyName System.Web + $decodedContent = [System.Web.HttpUtility]::HtmlDecode($fileListResponse.Content) + $fileListResponseXml = [xml]$decodedContent + Write-Host "Successfully received and DECODED Step 2 response." -ForegroundColor Green + + $fileIdentityMap = @{} + + # Get the two main lists of updates from the now correctly-decoded response + $newUpdates = $fileListResponseXml.Envelope.Body.SyncUpdatesResponse.SyncUpdatesResult.NewUpdates.UpdateInfo + $allExtendedUpdates = $fileListResponseXml.Envelope.Body.SyncUpdatesResponse.SyncUpdatesResult.ExtendedUpdateInfo.Updates.Update + + Write-Host "--- Correlating Update Information ---" -ForegroundColor Magenta + + # Filter the 'NewUpdates' list to only include items that are actual downloadable files. + # These are identified by the presence of the tag inside their inner XML. + $downloadableUpdates = $newUpdates | Where-Object { $_.Xml.Properties.SecuredFragment } + + Write-Host "Found $($downloadableUpdates.Count) potentially downloadable packages." -ForegroundColor Cyan + + # Now, process each downloadable update + foreach ($update in $downloadableUpdates) { + $lookupId = $update.ID + + # Find the matching entry in the 'ExtendedUpdateInfo' list using the same numeric ID. + $extendedInfo = $allExtendedUpdates | Where-Object { $_.ID -eq $lookupId } | Select-Object -First 1 + + if (-not $extendedInfo) { + Write-Warning "Could not find matching ExtendedInfo for downloadable update ID $lookupId. Skipping." + continue + } + + # From the extended info, get the actual package file and ignore the metadata .cab files. + $fileNode = $extendedInfo.Xml.Files.File | Where-Object { $_.FileName -and $_.FileName -notlike "Abm_*" } | Select-Object -First 1 + + if (-not $fileNode) { + Write-Warning "Found matching ExtendedInfo for ID $lookupId, but it contains no valid file node. Skipping." + continue + } + + # Additional parsing + $fileName = $fileNode.FileName + $updateGuid = $update.Xml.UpdateIdentity.UpdateID + $revNum = $update.Xml.UpdateIdentity.RevisionNumber + $fullIdentifier = $fileNode.GetAttribute("InstallerSpecificIdentifier") + + # Define the regex based on the official package identity structure. + # ____ + $regex = "^(?.+?)_(?\d+\.\d+\.\d+\.\d+)_(?[a-zA-Z0-9]+)_(?.*?)_(?[a-hjkmnp-tv-z0-9]{13})$" + + $packageInfo = [PSCustomObject]@{ + FullName = $fullIdentifier + FileName = $fileName + UpdateID = $updateGuid + RevisionNumber = $revNum + } + + if ($fullIdentifier -match $regex) { + # If the regex matches, populate the object with the named capture groups + $packageInfo | Add-Member -MemberType NoteProperty -Name "PackageName" -Value $matches.Name + $packageInfo | Add-Member -MemberType NoteProperty -Name "Version" -Value $matches.Version + $packageInfo | Add-Member -MemberType NoteProperty -Name "Architecture" -Value $matches.Architecture + $packageInfo | Add-Member -MemberType NoteProperty -Name "ResourceId" -Value $matches.ResourceId + $packageInfo | Add-Member -MemberType NoteProperty -Name "PublisherId" -Value $matches.PublisherId + } else { + # Fallback for any identifiers that don't match the pattern + $packageInfo | Add-Member -MemberType NoteProperty -Name "PackageName" -Value "Unknown (Parsing Failed)" + $packageInfo | Add-Member -MemberType NoteProperty -Name "Architecture" -Value "unknown" + } + + # Use the full, unique identifier as the key in the map + $fileIdentityMap[$fullIdentifier] = $packageInfo + + Write-Host " -> CORRELATED: '$($packageInfo.PackageName)' ($($packageInfo.Architecture))" -ForegroundColor Green + } + + Write-Host "--- Correlation Complete ---" -ForegroundColor Magenta + Write-Host "Found and prepared $($fileIdentityMap.Count) downloadable files." -ForegroundColor Green + + + # --- Step 3: Filter, Get URLs, and Download --- + try { + # Get the current system's processor architecture and map it to the script's naming convention + $systemArch = switch ($env:PROCESSOR_ARCHITECTURE) { + "AMD64" { "x64" } + "ARM64" { "arm64" } + "x86" { "x86" } + default { "unknown" } + } + + if ($systemArch -eq "unknown") { + throw "Could not determine system architecture from '$($env:PROCESSOR_ARCHITECTURE)'." + } + Write-Host "Step 3: Filtering packages for your system architecture ('$systemArch')..." -ForegroundColor Magenta + + # --- Filter the packages --- + + # 1. Isolate the Microsoft.WindowsStore packages and find the latest version + $latestStorePackage = $fileIdentityMap.Values | + Where-Object { $_.PackageName -eq 'Microsoft.WindowsStore' } | + Sort-Object { [version]$_.Version } -Descending | + Select-Object -First 1 + + # 2. Get all other dependencies that match the system architecture (or are neutral) + $filteredDependencies = $fileIdentityMap.Values | + Where-Object { + ($_.PackageName -ne 'Microsoft.WindowsStore') -and + ( ($_.Architecture -eq $systemArch) -or ($_.Architecture -eq 'neutral') ) + } + + # 3. Combine the lists for the final download queue + $packagesToDownload = @() + if ($latestStorePackage) { + $packagesToDownload += $latestStorePackage + Write-Host " -> Found latest Store package: $($latestStorePackage.FullName)" -ForegroundColor Green + } else { + Write-Warning "Could not find any Microsoft.WindowsStore package." + } + + $packagesToDownload += $filteredDependencies + Write-Host " -> Found $($filteredDependencies.Count) dependencies for '$systemArch' architecture." -ForegroundColor Green + Write-Host "Total files to download: $($packagesToDownload.Count)" -ForegroundColor Cyan + Write-Host "------------------------------------------------------------" + + + # --- Loop through the filtered list, get URLs, and download --- + Write-Host "Step 4: Fetching URLs and downloading files..." -ForegroundColor Magenta + + $originalPref = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + + foreach ($package in $packagesToDownload) { + Write-Host "Processing: $($package.FullName)" + + # Get the download URL for this specific package + $fileUrlRequestPayload = $fileUrlXmlTemplate -f $encryptedCookieData, $package.UpdateID, $package.RevisionNumber, $flightRing + $fileUrlResponse = Invoke-WebRequest -Uri "$baseUri/secured" -Method Post -Body $fileUrlRequestPayload -Headers $headers -UseBasicParsing + $fileUrlResponseXml = [xml]$fileUrlResponse.Content + + $fileLocations = $fileUrlResponseXml.Envelope.Body.GetExtendedUpdateInfo2Response.GetExtendedUpdateInfo2Result.FileLocations.FileLocation + $baseFileName = [System.IO.Path]::GetFileNameWithoutExtension($package.FileName) + $downloadUrl = ($fileLocations | Where-Object { $_.Url -like "*$baseFileName*" }).Url + + if (-not $downloadUrl) { + Write-Warning " -> Could not retrieve download URL for $($package.FileName). Skipping." + continue + } + + # Download the file + # Construct a more descriptive filename using the package's full name and its original extension + $fileExtension = [System.IO.Path]::GetExtension($package.FileName) + $newFileName = "$($package.FullName)$($fileExtension)" + $filePath = Join-Path $workingDir $newFileName + + Write-Host " -> Downloading from: $downloadUrl" -ForegroundColor Gray + Write-Host " -> Saving to: $filePath" + + try { + Invoke-WebRequest -Uri $downloadUrl -OutFile $filePath -UseBasicParsing + Write-Host " -> SUCCESS: Download complete." -ForegroundColor Green + } catch { + Write-Error " -> FAILED to download $($newFileName). Error: $($_.Exception.Message)" + } + Write-Host "" + } + + $ProgressPreference = $originalPref + + Write-Host "------------------------------------------------------------" + Write-Host "Process complete. All files are in the '$downloadDir' folder." -ForegroundColor Green + + } catch { + Write-Host "An error occurred during the filtering or downloading phase:" -ForegroundColor Red + Write-Host $_.Exception.ToString() + } + + # --- Step 5: Install Downloaded Packages --- + Write-Host "------------------------------------------------------------" + Write-Host "Step 5: Installing packages..." -ForegroundColor Magenta + Write-Host "This step requires Administrator privileges." + + # 1. Check for Administrator rights + if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Write-Error "Installation failed. Please re-run the script with 'Run as Administrator'." + # Add a pause so the user can see the message before the window closes. + Read-Host "Press Enter to exit" + exit + } + + # 2. Define the installation order for dependencies based on their base names + # The order here is critical for dependencies. + $dependencyInstallOrder = @( + 'Microsoft.VCLibs', + 'Microsoft.NET.Native.Framework', + 'Microsoft.NET.Native.Runtime', + 'Microsoft.UI.Xaml' + ) + + # 3. Get all downloaded package files and separate the main app from dependencies + try { + $allDownloadedFiles = Get-ChildItem -Path $workingDir -File | Where-Object { $_.Extension -in '.appx', '.msix', '.appxbundle', '.msixbundle' } + + $storePackageFile = $allDownloadedFiles | Where-Object { $_.Name -like 'Microsoft.WindowsStore*' } | Select-Object -First 1 + $dependencyFiles = $allDownloadedFiles | Where-Object { $_.Name -notlike 'Microsoft.WindowsStore*' } + + if (-not $dependencyFiles -and -not $storePackageFile) { + Write-Warning "No package files found in '$downloadDir' to install." + return # Exits this part of the script gracefully + } + + # 4. Install dependencies in the correct, predefined order + Write-Host "Installing dependencies..." + foreach ($baseName in $dependencyInstallOrder) { + # Find all packages that start with the current base name (e.g., 'Microsoft.VCLibs*') + # Sorting by name ensures a consistent order if multiple versions exist + $packagesInGroup = $dependencyFiles | Where-Object { $_.Name -like "$baseName*" } | Sort-Object Name + + foreach ($package in $packagesInGroup) { + Write-Host " -> Installing $($package.Name)" + try { + Add-AppxPackage -Path $package.FullName + Write-Host " SUCCESS." -ForegroundColor Green + } catch { + Write-Error " FAILED to install $($package.Name). Error: $($_.Exception.Message)" + } + } + } + + # 5. Install the main Microsoft Store package last + if ($storePackageFile) { + Write-Host "Installing the main application..." + Write-Host " -> Installing $($storePackageFile.Name)" + try { + Add-AppxPackage -Path $storePackageFile.FullName + Write-Host " SUCCESS: Microsoft Store has been installed/updated." -ForegroundColor Green + } catch { + Write-Error " FAILED to install $($storePackageFile.Name). Error: $($_.Exception.Message)" + } + } else { + Write-Warning "Microsoft Store package was not found in the download folder." + } + + Write-Host "------------------------------------------------------------" + Write-Host "Installation process finished." -ForegroundColor Cyan + + } catch { + Write-Error "A critical error occurred during the installation phase: $($_.Exception.Message)" + } + + # --- Set Region to US so the store will work. Default 'World' region does not work. --- + try { + # Define the path to the registry key + $geoKeyPath = "HKCU:\Control Panel\International\Geo" + + # Check if the 'Geo' key exists. If not, create it. + # The -Force switch ensures that parent keys ('International') are also created if they are missing. + if (-not (Test-Path $geoKeyPath)) { + Write-Host " -> Registry key not found. Creating: $geoKeyPath" + New-Item -Path $geoKeyPath -Force | Out-Null + } + + # Set the 'Nation' value. This is equivalent to: reg add ... /v Nation ... + Set-ItemProperty -Path $geoKeyPath -Name "Nation" -Value "244" + Write-Host " -> Set 'Nation' value to '244'." + + # Set the 'Name' value. This is equivalent to: reg add ... /v Name ... + Set-ItemProperty -Path $geoKeyPath -Name "Name" -Value "US" + Write-Host " -> Set 'Name' value to 'US'." + + Write-Host " -> Registry configuration complete." -ForegroundColor Green + } + catch { + Write-Error "FAILED to configure registry settings. Error: $($_.Exception.Message)" + } + + +} catch { + Write-Host "An error occurred:" -ForegroundColor Red + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode.value__ + $statusDescription = $_.Exception.Response.StatusDescription + $errorLogPath = Join-Path $LogDirectory "ERROR_Response.txt" + try { + $stream = $_.Exception.Response.GetResponseStream() + $reader = New-Object System.IO.StreamReader($stream) + $responseBody = $reader.ReadToEnd() + #$responseBody | Set-Content -Path $errorLogPath + } catch { "Could not read error response body." | Set-Content -Path $errorLogPath } + Write-Host "Status Code: $statusCode" + Write-Host "Status Description: $statusDescription" + Write-Host "Server Response saved to '$errorLogPath'" + } else { + Write-Host $_.Exception.ToString() + } +} diff --git a/Installer Scripts/Install-Winget.ps1 b/Installer Scripts/Install-Winget.ps1 new file mode 100644 index 0000000..dd16923 --- /dev/null +++ b/Installer Scripts/Install-Winget.ps1 @@ -0,0 +1,156 @@ +# This script will install Winget from within the Windows Sandbox +# It fetches the necessary files and dependencies from Microsoft's winget-cli, and installs them + +# Author: ThioJoe +# Repo Url: https://github.com/ThioJoe/Windows-Sandbox-Tools + + +param( + [switch]$removeMsStoreAsSource = $false # If switch is included, it will remove the 'msstore' source after installing winget, which doesn't work with Sandbox, unless the Microsoft Store is also installed + ) + +function Get-LatestRelease { + param( + [string]$repoOwner = 'microsoft', + [string]$repoName = 'winget-cli' + ) + try { + $releasesUrl = "https://api.github.com/repos/$repoOwner/$repoName/releases" + $releases = Invoke-RestMethod -Uri $releasesUrl -UseBasicParsing + } catch { + Write-Error "Failed to fetch releases from GitHub API: $($_.Exception.Message)" + return $null + } + + if (-not $releases) { Write-Error "No releases found for $repoOwner/$repoName."; return $null; } + + # Pick the top entry once sorted by published_at descending + $latestRelease = $releases | Sort-Object -Property published_at -Descending | Select-Object -First 1 + return $latestRelease +} + +function Get-AssetUrl { + param( + [Parameter(Mandatory=$true)] + $release, + [Parameter(Mandatory=$true)] + [string]$assetName + ) + + if ($release.assets -and $release.assets.Count -gt 0) { + $asset = $release.assets | Where-Object { $_.name -eq $assetName } + if ($asset) { + return $asset.browser_download_url + } + } + return $null +} + +function Install-WingetDependencies { + param([string]$depsFolder) + + # Look for DesktopAppInstaller_Dependencies.json to determine explicit install order + $jsonFile = Join-Path $depsFolder "DesktopAppInstaller_Dependencies.json" + if (Test-Path $jsonFile) { + Write-Host "Installing dependencies based on DesktopAppInstaller_Dependencies.json" + $jsonContent = Get-Content $jsonFile -Raw | ConvertFrom-Json + $dependencies = $jsonContent.Dependencies + + foreach ($dep in $dependencies) { + # For example: "Microsoft.VCLibs.140.00.UWPDesktop" + "14.0.33728.0" + $matchingFiles = Get-ChildItem -Path $depsFolder -Filter *.appx -Recurse | + Where-Object { $_.Name -like "*$($dep.Name)*" -and $_.Name -like "*$($dep.Version)*" } + + foreach ($file in $matchingFiles) { + Write-Host "Installing dependency: $($file.Name)" + Add-AppxPackage -Path $file.FullName + } + } + } + else { + # If the JSON doesn't exist, install all .appx in the folder + Write-Warning "No DesktopAppInstaller_Dependencies.json found, installing all .appx in $depsFolder" + foreach ($appxFile in Get-ChildItem $depsFolder -Filter *.appx -Recurse) { + Write-Host "Installing: $($appxFile.Name)" + Add-AppxPackage -Path $appxFile.FullName + } + } +} + +# Prevents progress bar from showing (often speeds downloads) +$ProgressPreference = 'SilentlyContinue' + +$downloadPath = Join-Path $env:USERPROFILE "Downloads" +$latestRelease = Get-LatestRelease +if (-not $latestRelease) { Write-Error "Could not retrieve the latest release. Exiting."; return; } + +$latestTag = $latestRelease.tag_name +Write-Host "Latest winget version tag is: $latestTag" + +# Download the MSIX bundle +$msixName = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" +$msixUrl = Get-AssetUrl -release $latestRelease -assetName $msixName +if (-not $msixUrl) { Write-Error "Could not find $msixName in the latest release assets."; return; } + +Write-Host "Downloading $msixName..." +$msixPath = Join-Path $downloadPath $msixName +Invoke-WebRequest -Uri $msixUrl -OutFile $msixPath + +# Figure out the OS architecture using environment variable +$procArch = $env:PROCESSOR_ARCHITECTURE +switch -Wildcard ($procArch) { + "AMD64" { $arch = "x64" } + "x86" { $arch = "x86" } + "*ARM64*" { $arch = "arm64" } + "*ARM*" { $arch = "arm" } + default { + $arch = "x64" + Write-Warning "Unrecognized architecture: $procArch. Defaulting to x64." + } +} + +# Download the dependencies zip +$depsZipName = "DesktopAppInstaller_Dependencies.zip" +$depsZipUrl = Get-AssetUrl -release $latestRelease -assetName $depsZipName + +# We'll expand to a base 'Dependencies' folder +$topDepsFolder = Join-Path $downloadPath "Dependencies" +# Then pick the sub-folder for the architecture +$depsFolder = Join-Path $topDepsFolder $arch + +if ($depsZipUrl) { + Write-Host "Downloading $depsZipName..." + $depsZipPath = Join-Path $downloadPath $depsZipName + Invoke-WebRequest -Uri $depsZipUrl -OutFile $depsZipPath + + # Remove existing Dependencies folder and expand the zip + if (Test-Path $topDepsFolder) { Remove-Item -Path $topDepsFolder -Recurse -Force } + + Expand-Archive -LiteralPath $depsZipPath -DestinationPath $topDepsFolder -Force +} +else { Write-Warning "No $depsZipName found in $latestTag, skipping dependency download."; } + +# Restore progress preference +$ProgressPreference = 'Continue' + +# If dependencies exist for this architecture, install them +if (Test-Path $depsFolder) { + Install-WingetDependencies -depsFolder $depsFolder +} else { + Write-Warning "No architecture-specific dependencies found at $depsFolder" +} + +# Finally, install the winget MSIX bundle +Write-Host "Installing $msixName..." +Add-AppxPackage -Path $msixPath + +# Remove msstore source if set to do so +if ($removeMsStoreAsSource.IsPresent) { + Write-Host "Attempting to remove 'msstore' source from winget..." + try { + winget source remove -n msstore --ignore-warnings + } catch { + Write-Warning "An error occurred while trying to execute 'winget source remove msstore': $($_.Exception.Message)" + } +} + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..210b917 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ThioJoe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..179c75b --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Windows-Sandbox-Tools +Various useful scripts for use within Windows Sandbox diff --git a/Sandbox Configurations/MyDefaultSandbox.wsb b/Sandbox Configurations/MyDefaultSandbox.wsb new file mode 100644 index 0000000..99b3e7a --- /dev/null +++ b/Sandbox Configurations/MyDefaultSandbox.wsb @@ -0,0 +1,45 @@ + + + + Enable + + Enable + 12288 + + + Disable + Disable + Disabled + Disable + + + Enable + + + + + + + B:\Caches\Sandbox_Share + C:\Users\WDAGUtilityAccount\Desktop\HostShared + true + + + + + + powershell -executionpolicy Bypass -command "start powershell {-file C:\Users\WDAGUtilityAccount\Desktop\HostShared\SandboxStartup.ps1}" + + + + + + + + diff --git a/Sandbox Configurations/UseGoogleDNS.wsb b/Sandbox Configurations/UseGoogleDNS.wsb new file mode 100644 index 0000000..44681e7 --- /dev/null +++ b/Sandbox Configurations/UseGoogleDNS.wsb @@ -0,0 +1,9 @@ + + + Enable + + + netsh interface ipv4 set dnsservers "Ethernet" static 8.8.8.8 primary + + + diff --git a/Startup Scripts/SandboxStartup.ps1 b/Startup Scripts/SandboxStartup.ps1 new file mode 100644 index 0000000..a9ea86f --- /dev/null +++ b/Startup Scripts/SandboxStartup.ps1 @@ -0,0 +1,50 @@ +# Change context menu to old style +reg.exe add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve +reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "Start_ShowClassicMode" /t REG_DWORD /d 1 /f + +# Show file extensions +reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "HideFileExt" /t REG_DWORD /d 0 /f + +# Show hidden files +reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "Hidden" /t REG_DWORD /d 1 /f + +# ---- Add 'Edit With Notepad' and 'Open Notepad' to context menu ------- +# Set the path to your specific Notepad executable which you'll put in the shared folder, since notepad isn't included in the sandbox for some reason +# Go to C:\Windows on your main computer and copy Notepad.exe, then copy notepad.exe.mui from your main language folder, such as C:\Windows\en-US +# Important: Notepad.exe.mui can't simply go next to notepad.exe. You need to actually create the language folder (like en-US) again next to notepad.exe and put it in that. Otherwise notepad won't run. +$notepadPath = "C:\Users\WDAGUtilityAccount\Desktop\HostShared\notepad.exe" +reg add "HKEY_CLASSES_ROOT\*\shell\Edit with Notepad" /f +reg add "HKEY_CLASSES_ROOT\*\shell\Edit with Notepad" /v "Icon" /t REG_SZ /d "$notepadPath,0" /f +reg add "HKEY_CLASSES_ROOT\*\shell\Edit with Notepad\command" /ve /d "`"$notepadPath`" `"%1`"" /f +reg add "HKEY_CLASSES_ROOT\Directory\Background\shell\Notepad" /ve /d "Open Notepad" /f +reg add "HKEY_CLASSES_ROOT\Directory\Background\shell\Notepad" /v "Icon" /t REG_SZ /d "$notepadPath,0" /f +reg add "HKEY_CLASSES_ROOT\Directory\Background\shell\Notepad\command" /ve /d "`"$notepadPath`"" /f +# ---- Add 'Open PowerShell Here' and 'Open CMD Here' to context menu ------- +$powershellPath = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" +$cmdPath = "C:\Windows\System32\cmd.exe" +reg add "HKEY_CLASSES_ROOT\Directory\Background\shell\MyPowerShell" /ve /d "Open PowerShell Here" /f +reg add "HKEY_CLASSES_ROOT\Directory\Background\shell\MyPowerShell" /v "Icon" /t REG_SZ /d "$powershellPath,0" /f +reg add "HKEY_CLASSES_ROOT\Directory\Background\shell\MyPowerShell\command" /ve /d "powershell.exe -noexit -command Set-Location -literalPath '%V'" /f +reg add "HKEY_CLASSES_ROOT\Directory\Background\shell\Mycmd" /ve /d "Open CMD Here" /f +reg add "HKEY_CLASSES_ROOT\Directory\Background\shell\Mycmd" /v "Icon" /t REG_SZ /d "$cmdPath,0" /f +reg add "HKEY_CLASSES_ROOT\Directory\Background\shell\Mycmd\command" /ve /d "cmd.exe /s /k cd /d `"\`"%V`"\`"" /f +# ---- Set .txt files to open with Notepad, Create txt option in Context Menu 'New' list +cmd /c assoc .txt=txtfile +cmd /c ftype txtfile=`"$notepadPath`" "%1" +reg add "HKEY_CLASSES_ROOT\txtfile" /ve /d "Text Document" /f +reg add "HKEY_CLASSES_ROOT\.txt\ShellNew" /f +reg add "HKEY_CLASSES_ROOT\.txt\ShellNew" /v "NullFile" /t REG_SZ /d "" /f +reg add "HKEY_CLASSES_ROOT\.txt\ShellNew" /v "ItemName" /t REG_SZ /d "New Text Document" /f +# ------------------------------------------------------------------------- + + +# Restart Explorer so changes take effect +Stop-Process -Name explorer -Force +# Open an explorer window to the host-shared folder +Start-Process explorer.exe C:\Users\WDAGUtilityAccount\Desktop\HostShared + +# Change execution policy for powershell to allow running scripts +Set-ExecutionPolicy -ExecutionPolicy unrestricted -Scope LocalMachine + +# Uncomment to pause after running +#Read-Host "Pause" diff --git a/Theme Scripts/Set Theme Dark Mode.ps1 b/Theme Scripts/Set Theme Dark Mode.ps1 new file mode 100644 index 0000000..6109ad5 --- /dev/null +++ b/Theme Scripts/Set Theme Dark Mode.ps1 @@ -0,0 +1,28 @@ +# Enable Dark Mode for Apps +Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize" -Name "AppsUseLightTheme" -Value 0 + +# Enable Dark Mode for System +Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize" -Name "SystemUsesLightTheme" -Value 0 + +# Set the Wallpaper +$wallpaperPath = "C:\Windows\Web\Wallpaper\Windows\img19.jpg" +$code = @' +using System.Runtime.InteropServices; +public class Wallpaper { + [DllImport("user32.dll", CharSet=CharSet.Auto)] + public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); +} +'@ + +Add-Type $code +$SPI_SETDESKWALLPAPER = 0x0014 +$UPDATE_INI_FILE = 0x01 +$SEND_CHANGE = 0x02 + +[Wallpaper]::SystemParametersInfo($SPI_SETDESKWALLPAPER, 0, $wallpaperPath, ($UPDATE_INI_FILE -bor $SEND_CHANGE)) + +# Restart Explorer to apply changes +Write-Host "Restarting Explorer..." +Stop-Process -Name explorer -Force +Start-Process explorer +Write-Host "Dark mode enabled and wallpaper updated successfully! Explorer has been restarted." diff --git a/Theme Scripts/Set Theme Light Mode.ps1 b/Theme Scripts/Set Theme Light Mode.ps1 new file mode 100644 index 0000000..9dc546e --- /dev/null +++ b/Theme Scripts/Set Theme Light Mode.ps1 @@ -0,0 +1,10 @@ +# Enable Light Mode for Apps +Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize" -Name "AppsUseLightTheme" -Value 1 +# Enable Light Mode for System +Set-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize" -Name "SystemUsesLightTheme" -Value 1 + +# Restart Explorer to apply changes +Write-Host "Restarting Explorer..." +Stop-Process -Name explorer -Force +Start-Process explorer +Write-Host "Light mode enabled! Explorer has been restarted."