From 7ce2bd1e8417a6cbef96a16a375aedebb67f55e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:58:19 +0000 Subject: [PATCH 1/6] Implement security hardening for PowerShell entrypoints Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com> --- README.md | 10 +- choco-pack-install.ps1 | 2 +- choco-package-explorer.ps1 | 2 +- choco-sync.ps1 | 2 +- choco-upgrade-interactive.ps1 | 2 +- choco-utils.ps1 | 2 +- list-choco-apps.ps1 | 2 +- scripts/choco-manager.ps1 | 4 +- scripts/main-menu.ps1 | 49 ++-- src/Choco/choco-pack-install.ps1 | 41 +-- src/Choco/choco-package-explorer.ps1 | 60 ++-- src/Choco/choco-sync.ps1 | 25 +- src/Choco/choco-upgrade-interactive.ps1 | 11 +- src/Choco/choco-utils.ps1 | 20 +- src/Choco/list-choco-apps.ps1 | 11 +- src/Core/core-functions.ps1 | 354 ++++++++++++++++++++++-- src/Winget/winget-utils.ps1 | 32 ++- winget-utils.ps1 | 2 +- 18 files changed, 500 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index a749e85..4c7e93f 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ This interactive tool allows you to check status, export lists, and install pack Notes: - If Chocolatey is missing, the main menu will show an "Install Chocolatey" option. - The footer shows the installed Chocolatey version and, when different, the latest available version. +- The wrappers now invoke the project scripts directly instead of forcing a blanket process-scope execution policy bypass. ## Project Layout @@ -96,10 +97,13 @@ The script will: ## Security Guidance - Run the scripts from a trusted, access-controlled directory. -- Prefer `RemoteSigned` or stronger execution policies when possible. If you must use process-scope bypass, ensure files are from a trusted source. -- Review `choco_packages.txt` before running install/sync actions. +- Prefer `RemoteSigned` or stronger execution policies. The project no longer forces routine `ExecutionPolicy Bypass` for normal menu and wrapper launches. +- Chocolatey installs, searches, and upgrades are restricted to `https://community.chocolatey.org/api/v2/` by default. +- Winget installs and searches are restricted to the `winget` source by default. +- Review `data/choco_packages.txt` before running install, update, or sync actions. Managed package list reads and writes are restricted to the repository `data\` directory. +- The Chocolatey bootstrap flow now downloads the package locally, shows its SHA256 hash, and requires explicit confirmation before executing the local installer. - Avoid copying scripts from unknown sources or locations with weak ACLs. -- Treat `choco-manager.log` as sensitive inventory data; restrict access where appropriate. +- Treat `logs/choco-manager.log` as sensitive inventory data. By default, only warning and error events are persisted; set `CHOCO_MANAGER_LOG_FILE_LEVELS` if you need broader local audit logging. ## Disclaimer diff --git a/choco-pack-install.ps1 b/choco-pack-install.ps1 index 2908204..5df75b3 100644 --- a/choco-pack-install.ps1 +++ b/choco-pack-install.ps1 @@ -1,2 +1,2 @@ # Wrapper script for choco-pack-install -powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-pack-install.ps1") @args +& (Join-Path $PSScriptRoot "src\Choco\choco-pack-install.ps1") @args diff --git a/choco-package-explorer.ps1 b/choco-package-explorer.ps1 index d6d4f60..c13878e 100644 --- a/choco-package-explorer.ps1 +++ b/choco-package-explorer.ps1 @@ -1,2 +1,2 @@ # Wrapper script for choco-package-explorer -powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-package-explorer.ps1") @args +& (Join-Path $PSScriptRoot "src\Choco\choco-package-explorer.ps1") @args diff --git a/choco-sync.ps1 b/choco-sync.ps1 index 187a085..a90916e 100644 --- a/choco-sync.ps1 +++ b/choco-sync.ps1 @@ -1,2 +1,2 @@ # Wrapper script for choco-sync -powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-sync.ps1") @args +& (Join-Path $PSScriptRoot "src\Choco\choco-sync.ps1") @args diff --git a/choco-upgrade-interactive.ps1 b/choco-upgrade-interactive.ps1 index 4e8b11d..3edd8f7 100644 --- a/choco-upgrade-interactive.ps1 +++ b/choco-upgrade-interactive.ps1 @@ -1,2 +1,2 @@ # Wrapper script for choco-upgrade-interactive -powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-upgrade-interactive.ps1") @args +& (Join-Path $PSScriptRoot "src\Choco\choco-upgrade-interactive.ps1") @args diff --git a/choco-utils.ps1 b/choco-utils.ps1 index 9d7043d..3f7f6dc 100644 --- a/choco-utils.ps1 +++ b/choco-utils.ps1 @@ -1,2 +1,2 @@ # Wrapper script for choco-utils -powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\choco-utils.ps1") @args +& (Join-Path $PSScriptRoot "src\Choco\choco-utils.ps1") @args diff --git a/list-choco-apps.ps1 b/list-choco-apps.ps1 index e6f5f05..24ba56e 100644 --- a/list-choco-apps.ps1 +++ b/list-choco-apps.ps1 @@ -1,2 +1,2 @@ # Wrapper script for list-choco-apps -powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Choco\list-choco-apps.ps1") @args +& (Join-Path $PSScriptRoot "src\Choco\list-choco-apps.ps1") @args diff --git a/scripts/choco-manager.ps1 b/scripts/choco-manager.ps1 index 6ad6b0c..3aa5301 100644 --- a/scripts/choco-manager.ps1 +++ b/scripts/choco-manager.ps1 @@ -1,6 +1,6 @@ # Entry script for Choco-Manager -# Load core functions with process-scope policy + unblock +# Load core functions $corePath = Join-Path (Resolve-Path (Join-Path $PSScriptRoot "..")) "src\Core\core-functions.ps1" if (-not (Test-Path $corePath)) { Write-Error "Core functions not found at $corePath" @@ -8,8 +8,6 @@ if (-not (Test-Path $corePath)) { } try { - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force - Unblock-File -Path $corePath -ErrorAction SilentlyContinue . $corePath } catch { Write-Error "Failed to load core functions: $($_.Exception.Message)" diff --git a/scripts/main-menu.ps1 b/scripts/main-menu.ps1 index 77fef92..2fcba7d 100644 --- a/scripts/main-menu.ps1 +++ b/scripts/main-menu.ps1 @@ -20,7 +20,11 @@ function Show-Footer { $chocoInfo = Get-ChocoVersionInfo $chocoVer = if ($chocoInfo.IsInstalled) { $chocoInfo.InstalledVersion } else { $null } $chocoLatest = $chocoInfo.LatestVersion - $wingetVer = winget --version 2>$null + $wingetVer = $null + try { + $wingetVer = ((Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("--version")) | Select-Object -First 1) + } + catch { } Write-Host "----------------------------" -ForegroundColor Gray if ($chocoVer) { if ($chocoLatest -and $chocoLatest -ne $chocoVer) { @@ -41,7 +45,7 @@ function Show-PackageList { Write-Host "`nFetching local packages..." -ForegroundColor Gray $items = @() - $chocoRaw = choco list -lo -r + $chocoRaw = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "-lo", "-r") foreach ($line in $chocoRaw) { if ($line -match '\|') { $parts = $line -split '\|' @@ -59,9 +63,8 @@ function Show-PackageList { } } - $wingetCmd = Get-Command winget -ErrorAction SilentlyContinue - if ($wingetCmd) { - $wingetRaw = winget list + try { + $wingetRaw = Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("list") if ($LASTEXITCODE -eq 0) { foreach ($line in $wingetRaw) { if ($line -match '^\s*Name\s+Id\s+Version') { continue } @@ -89,6 +92,9 @@ function Show-PackageList { Write-Log "Winget list failed (Exit Code: $LASTEXITCODE)." "WARN" } } + catch { + Write-Log "Winget list failed: $($_.Exception.Message)" "WARN" + } if ($items.Count -eq 0) { Write-Log "No local packages found." "WARN" @@ -115,14 +121,14 @@ function Show-PackageList { $safeId = Get-ValidatedPackageId -Id $item.Id -Context "Winget" if ($safeId) { Write-Host "`n--- Winget Info for $safeId ---" -ForegroundColor Yellow - winget show --id $safeId + Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("show", "--id", $safeId, "--exact", "--source", (Get-TrustedWingetSource)) Pause } } else { $safeId = Get-ValidatedPackageId -Id $item.Id -Context "Chocolatey" if ($safeId) { Write-Host "`n--- Chocolatey Info for $safeId ---" -ForegroundColor Yellow - choco info $safeId + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("info", $safeId, "--source", (Get-TrustedChocolateySource)) Pause } } @@ -181,7 +187,7 @@ do { "help" { Show-CommandHelp; Pause } "quit" { return } "list" { Show-PackageList; Pause } - "search" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") } + "search" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") } "logs" { Show-AuditLog; Pause } Default { if ($cmd) { Write-Host "Unknown command: $cmd" -ForegroundColor Yellow; Pause } @@ -192,22 +198,25 @@ do { switch ($choice) { "List/View Packages (Combined)" { Show-PackageList; Pause } - "List Local Packages (Choco)" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") -Action ListChoco; Pause } - "List Local Packages (Winget)" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -Action ListOnly; Pause } - "Export/Update List from Local" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\list-choco-apps.ps1"); Pause } - "Install Missing Packages" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-pack-install.ps1"); Pause } - "Synchronize (Full Match)" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-sync.ps1") -Action Sync; Pause } - "Interactive Update (Selectable)" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-upgrade-interactive.ps1"); Pause } - "Update All Packages (Silent)" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-utils.ps1") -Action Update; Pause } - "Search Chocolatey Repository" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") } - "Search Winget Repository" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -Action Search } + "List Local Packages (Choco)" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") -ArgumentList @("-Action", "ListChoco"); Pause } + "List Local Packages (Winget)" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -ArgumentList @("-Action", "ListOnly"); Pause } + "Export/Update List from Local" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\list-choco-apps.ps1"); Pause } + "Install Missing Packages" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-pack-install.ps1"); Pause } + "Synchronize (Full Match)" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-sync.ps1") -ArgumentList @("-Action", "Sync"); Pause } + "Interactive Update (Selectable)" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-upgrade-interactive.ps1"); Pause } + "Update All Packages (Silent)" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-utils.ps1") -ArgumentList @("-Action", "Update"); Pause } + "Search Chocolatey Repository" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") } + "Search Winget Repository" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -ArgumentList @("-Action", "Search") } "Package Info (by name)" { $pkg = Read-Host "Enter package name" $safePkg = Get-ValidatedPackageId -Id $pkg -Context "Chocolatey" - if ($safePkg) { choco info $safePkg; Pause } + if ($safePkg) { + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("info", $safePkg, "--source", (Get-TrustedChocolateySource)) + Pause + } } - "Package Utilities" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") } - "Winget Tools" { powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -Action Interactive } + "Package Utilities" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Choco\choco-package-explorer.ps1") } + "Winget Tools" { Invoke-ScriptFile -FilePath (Join-Path $PSScriptRoot "..\src\Winget\winget-utils.ps1") -ArgumentList @("-Action", "Interactive") } "View Audit Log" { Show-AuditLog; Pause } "Elevate to Admin" { Invoke-ElevatedAction -FilePath (Join-Path $PSScriptRoot "choco-manager.ps1") diff --git a/src/Choco/choco-pack-install.ps1 b/src/Choco/choco-pack-install.ps1 index 884c9de..8437a45 100644 --- a/src/Choco/choco-pack-install.ps1 +++ b/src/Choco/choco-pack-install.ps1 @@ -8,10 +8,11 @@ param( . (Join-Path (Resolve-Path (Join-Path $PSScriptRoot "..\..")) "src\Core\core-functions.ps1") if (-not $InputFile) { - $scriptRoot = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } - $InputFile = Join-Path (Resolve-Path (Join-Path $scriptRoot "..\..")) "data\choco_packages.txt" + $InputFile = Get-DefaultPackageListPath } +$InputFile = Resolve-ManagedDataFilePath -Path $InputFile -Purpose "package list" + if (-not (Test-IsAdmin)) { Write-Log "Elevation required for installation. Re-launching..." "WARN" Invoke-ElevatedAction -FilePath $MyInvocation.MyCommand.Path -ArgumentList @("-InputFile", $InputFile) @@ -22,35 +23,43 @@ Write-Log "Starting installation process..." "INFO" if (-not $SkipUpgradeChoco) { Write-Log "Checking for Chocolatey updates..." "INFO" - choco upgrade chocolatey -y + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("upgrade", "chocolatey", "-y", "--source", (Get-TrustedChocolateySource)) } # Get currently installed packages for comparison -$installedPackages = choco list --local-only --limit-output | ForEach-Object { $_.Split('|')[0] } +$installedPackages = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "--local-only", "--limit-output") | ForEach-Object { $_.Split('|')[0] } # Read target packages using core helper - $targetPackages = Get-PackageList -Path $InputFile +$targetPackages = Get-PackageList -Path $InputFile if ($targetPackages.Count -eq 0) { Write-Log "No packages found in $InputFile to install." "WARN" exit } -foreach ($packageName in $targetPackages) { +$missingPackages = $targetPackages | Where-Object { $installedPackages -notcontains $_ } +if ($missingPackages.Count -eq 0) { + Write-Log "All packages from $InputFile are already installed." "SUCCESS" + exit +} + +Write-Host "Trusted Chocolatey source: $(Get-TrustedChocolateySource)" -ForegroundColor DarkGray +if (-not (Read-Confirmation -Prompt "Install $($missingPackages.Count) missing package(s)? Type y to continue" -ExpectedValue "y")) { + Write-Log "Installation cancelled by user." "WARN" + exit +} + +foreach ($packageName in $missingPackages) { $safeName = Get-ValidatedPackageId -Id $packageName -Context "Chocolatey" if (-not $safeName) { continue } - if ($installedPackages -contains $safeName) { - Write-Log "Skipping '$safeName' - already installed." "INFO" + + Write-Log "Installing '$safeName'..." "INFO" + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("install", $safeName, "-y", "--source", (Get-TrustedChocolateySource)) + if ($LASTEXITCODE -eq 0) { + Write-Log "Successfully installed '$safeName'." "SUCCESS" } else { - Write-Log "Installing '$safeName'..." "INFO" - choco install $safeName -y - if ($LASTEXITCODE -eq 0) { - Write-Log "Successfully installed '$safeName'." "SUCCESS" - } - else { - Write-Log "Failed to install '$safeName' (Exit Code: $LASTEXITCODE)." "ERROR" - } + Write-Log "Failed to install '$safeName' (Exit Code: $LASTEXITCODE)." "ERROR" } } diff --git a/src/Choco/choco-package-explorer.ps1 b/src/Choco/choco-package-explorer.ps1 index dd4d3b9..630453d 100644 --- a/src/Choco/choco-package-explorer.ps1 +++ b/src/Choco/choco-package-explorer.ps1 @@ -12,7 +12,7 @@ if (Test-Path $corePath) { . $corePath } function Show-ChocoLocalPackages { Write-Host "`nFetching local Chocolatey packages..." -ForegroundColor Gray - $raw = choco list -lo -r + $raw = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "-lo", "-r") $packages = @() foreach ($line in $raw) { if ($line -match '\|') { @@ -44,7 +44,7 @@ function Show-ChocoLocalPackages { $safeId = Get-ValidatedPackageId -Id $pkgId -Context "Chocolatey" if ($safeId) { Write-Host "`n--- Info for $safeId ---" -ForegroundColor Yellow - choco info $safeId + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("info", $safeId, "--source", (Get-TrustedChocolateySource)) Pause } } @@ -52,14 +52,16 @@ function Show-ChocoLocalPackages { function Show-WingetLocalPackages { Write-Host "`nFetching local Winget packages..." -ForegroundColor Gray - $wingetCmd = Get-Command winget -ErrorAction SilentlyContinue - if (-not $wingetCmd) { + try { + $null = Resolve-TrustedCommandPath -CommandName "winget" + } + catch { Write-Host "Winget not found on this system." -ForegroundColor Yellow Pause return } - $raw = winget list + $raw = Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("list") if ($LASTEXITCODE -ne 0) { Write-Host "Winget list failed (Exit Code: $LASTEXITCODE)." -ForegroundColor Yellow Pause @@ -105,7 +107,7 @@ function Show-WingetLocalPackages { $safeId = Get-ValidatedPackageId -Id $pkgId -Context "Winget" if ($safeId) { Write-Host "`n--- Info for $safeId ---" -ForegroundColor Yellow - winget show --id $safeId + Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("show", "--id", $safeId, "--exact", "--source", (Get-TrustedWingetSource)) Pause } } @@ -115,7 +117,7 @@ function Show-CombinedLocalPackages { Write-Host "`nFetching local packages (Choco + Winget)..." -ForegroundColor Gray $items = @() - $chocoRaw = choco list -lo -r + $chocoRaw = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "-lo", "-r") foreach ($line in $chocoRaw) { if ($line -match '\|') { $parts = $line -split '\|' @@ -127,9 +129,8 @@ function Show-CombinedLocalPackages { } } - $wingetCmd = Get-Command winget -ErrorAction SilentlyContinue - if ($wingetCmd) { - $wingetRaw = winget list + try { + $wingetRaw = Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("list") if ($LASTEXITCODE -eq 0) { foreach ($line in $wingetRaw) { if ($line -match '^\s*Name\s+Id\s+Version') { continue } @@ -149,6 +150,7 @@ function Show-CombinedLocalPackages { } } } + catch { } if ($items.Count -eq 0) { Write-Host "No local packages found." -ForegroundColor Yellow @@ -176,14 +178,14 @@ function Show-CombinedLocalPackages { $safeId = Get-ValidatedPackageId -Id $item.Id -Context "Winget" if ($safeId) { Write-Host "`n--- Winget Info for $safeId ---" -ForegroundColor Yellow - winget show --id $safeId + Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("show", "--id", $safeId, "--exact", "--source", (Get-TrustedWingetSource)) Pause } } else { $safeId = Get-ValidatedPackageId -Id $item.Id -Context "Chocolatey" if ($safeId) { Write-Host "`n--- Chocolatey Info for $safeId ---" -ForegroundColor Yellow - choco info $safeId + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("info", $safeId, "--source", (Get-TrustedChocolateySource)) Pause } } @@ -202,7 +204,7 @@ function Search-Packages { $results = @() if ($Repo -eq "Chocolatey") { - $raw = choco search $keyword -r + $raw = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("search", $keyword, "-r", "--source", (Get-TrustedChocolateySource)) foreach ($line in $raw) { if ($line -match '\|') { $parts = $line -split '\|' @@ -213,10 +215,16 @@ function Search-Packages { } } else { # Winget Search - $raw = winget search $keyword - winget search $keyword + $raw = Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("search", $keyword, "--source", (Get-TrustedWingetSource)) + $raw $id = Read-Host "`nEnter Package ID for more info (or press Enter to skip)" - if ($id) { winget show $id; Pause } + if ($id) { + $safeId = Get-ValidatedPackageId -Id $id -Context "Winget" + if ($safeId) { + Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("show", "--id", $safeId, "--exact", "--source", (Get-TrustedWingetSource)) + } + Pause + } return } @@ -241,14 +249,14 @@ function Search-Packages { $safePkgId = Get-ValidatedPackageId -Id $pkgId -Context "Chocolatey" if (-not $safePkgId) { Pause; return } Write-Host "`n--- Info for $pkgId ---" -ForegroundColor Yellow - choco info $safePkgId + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("info", $safePkgId, "--source", (Get-TrustedChocolateySource)) - $ins = Read-Host "`nWould you like to install $pkgId? (y/n)" - if ($ins -eq 'y') { + Write-Host "Trusted Chocolatey source: $(Get-TrustedChocolateySource)" -ForegroundColor DarkGray + if (Read-Confirmation -Prompt "`nInstall $pkgId from the approved source? Type y to continue" -ExpectedValue "y") { if (-not (Test-IsAdmin)) { - Invoke-ElevatedProcess -FilePath "choco" -ArgumentList @("install", $safePkgId, "-y") + Invoke-ElevatedProcess -FilePath "choco" -ArgumentList @("install", $safePkgId, "-y", "--source", (Get-TrustedChocolateySource)) } else { - choco install $safePkgId -y + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("install", $safePkgId, "-y", "--source", (Get-TrustedChocolateySource)) } } Pause @@ -304,7 +312,10 @@ do { "Package Info (by name)" { $pkg = Read-Host "Enter package name" $safePkg = Get-ValidatedPackageId -Id $pkg -Context "Chocolatey" - if ($safePkg) { choco info $safePkg; Pause } + if ($safePkg) { + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("info", $safePkg, "--source", (Get-TrustedChocolateySource)) + Pause + } } "List Local Packages (Combined)" { Show-CombinedLocalPackages } "List Local Packages (Choco)" { Show-ChocoLocalPackages } @@ -313,12 +324,11 @@ do { $pkg = Read-Host "Enter package name to uninstall" $safePkg = Get-ValidatedPackageId -Id $pkg -Context "Chocolatey" if ($safePkg) { - $confirm = Read-Host "Are you sure you want to uninstall $safePkg? (y/n)" - if ($confirm -eq 'y') { + if (Read-Confirmation -Prompt "Uninstall $safePkg? Type y to continue" -ExpectedValue "y") { if (-not (Test-IsAdmin)) { Invoke-ElevatedProcess -FilePath "choco" -ArgumentList @("uninstall", $safePkg, "-y") } else { - choco uninstall $safePkg -y + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("uninstall", $safePkg, "-y") } } } diff --git a/src/Choco/choco-sync.ps1 b/src/Choco/choco-sync.ps1 index 95095ed..3b6615c 100644 --- a/src/Choco/choco-sync.ps1 +++ b/src/Choco/choco-sync.ps1 @@ -12,10 +12,11 @@ param( . (Join-Path (Resolve-Path (Join-Path $PSScriptRoot "..\..")) "src\Core\core-functions.ps1") if (-not $InputFile) { - $scriptRoot = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } - $InputFile = Join-Path (Resolve-Path (Join-Path $scriptRoot "..\..")) "data\choco_packages.txt" + $InputFile = Get-DefaultPackageListPath } +$InputFile = Resolve-ManagedDataFilePath -Path $InputFile -Purpose "package list" + if (-not (Test-IsAdmin)) { Write-Log "Elevation required for $Action. Re-launching..." "WARN" Invoke-ElevatedAction -FilePath $MyInvocation.MyCommand.Path -ArgumentList @("-Action", $Action, "-PackageName", $PackageName, "-InputFile", $InputFile) @@ -32,7 +33,7 @@ if ($Action -eq "Remove") { if (-not $safeName) { return } Write-Log "Uninstalling package: $safeName..." "INFO" - choco uninstall $safeName -y + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("uninstall", $safeName, "-y") if ($LASTEXITCODE -eq 0) { Write-Log "Successfully uninstalled $safeName. Updating list..." "SUCCESS" @@ -48,15 +49,22 @@ elseif ($Action -eq "Sync") { Write-Log "Synchronizing local system with $InputFile..." "INFO" $targetPackages = Get-PackageList -Path $InputFile - $installedPackages = choco list --local-only --limit-output | ForEach-Object { $_.Split('|')[0] } + $installedPackages = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "--local-only", "--limit-output") | ForEach-Object { $_.Split('|')[0] } # 1. Install missing $toInstall = $targetPackages | Where-Object { $installedPackages -notcontains $_ } + if ($toInstall.Count -gt 0) { + Write-Host "Trusted Chocolatey source: $(Get-TrustedChocolateySource)" -ForegroundColor DarkGray + if (-not (Read-Confirmation -Prompt "Install $($toInstall.Count) missing package(s) from the approved source? Type y to continue" -ExpectedValue "y")) { + Write-Log "Sync install phase cancelled by user." "WARN" + $toInstall = @() + } + } foreach ($p in $toInstall) { $safeName = Get-ValidatedPackageId -Id $p -Context "Chocolatey" if (-not $safeName) { continue } Write-Log "Sync: Installing missing package $safeName..." "INFO" - choco install $safeName -y + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("install", $safeName, "-y", "--source", (Get-TrustedChocolateySource)) if ($LASTEXITCODE -ne 0) { Write-Log "Sync: Failed to install $safeName (Exit Code: $LASTEXITCODE)." "ERROR" } @@ -70,17 +78,18 @@ elseif ($Action -eq "Sync") { if ($toRemove) { Write-Log "Found orphaned packages: $($toRemove -join ', ')" "WARN" - $confirm = Read-Host "Remove these packages to match list? (y/n)" - if ($confirm -eq 'y') { + if (Read-Confirmation -Prompt "Remove $($toRemove.Count) orphaned package(s) to match the approved list? Type y to continue" -ExpectedValue "y") { foreach ($p in $toRemove) { $safeName = Get-ValidatedPackageId -Id $p -Context "Chocolatey" if (-not $safeName) { continue } Write-Log "Sync: Removing orphaned package $safeName..." "INFO" - choco uninstall $safeName -y + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("uninstall", $safeName, "-y") if ($LASTEXITCODE -ne 0) { Write-Log "Sync: Failed to uninstall $safeName (Exit Code: $LASTEXITCODE)." "ERROR" } } + } else { + Write-Log "Sync remove phase cancelled by user." "WARN" } } diff --git a/src/Choco/choco-upgrade-interactive.ps1 b/src/Choco/choco-upgrade-interactive.ps1 index 2283435..b1406cf 100644 --- a/src/Choco/choco-upgrade-interactive.ps1 +++ b/src/Choco/choco-upgrade-interactive.ps1 @@ -15,7 +15,7 @@ if (-not (Test-IsAdmin)) { # 3. Get and Parse Outdated Packages Write-Log "Checking for outdated packages (this may take a moment)..." "INFO" # -r (or --limit-output) returns: name|current|available|pinned -$outdatedRaw = choco outdated -r +$outdatedRaw = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("outdated", "-r", "--source", (Get-TrustedChocolateySource)) $outdatedPackages = @() foreach ($line in $outdatedRaw) { @@ -64,7 +64,12 @@ if ($selection -match 'q') { } elseif ($selection -match 'a') { Write-Log "Upgrading all outdated packages..." "INFO" - choco upgrade all -y + Write-Host "Trusted Chocolatey source: $(Get-TrustedChocolateySource)" -ForegroundColor DarkGray + if (-not (Read-Confirmation -Prompt "Upgrade all outdated packages from the approved source? Type y to continue" -ExpectedValue "y")) { + Write-Log "Upgrade all cancelled by user." "WARN" + return + } + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("upgrade", "all", "-y", "--source", (Get-TrustedChocolateySource)) } else { # Parse comma-separated input @@ -83,7 +88,7 @@ else { $safeName = Get-ValidatedPackageId -Id $pkg -Context "Chocolatey" if (-not $safeName) { continue } Write-Log "Starting upgrade for $safeName..." "INFO" - choco upgrade $safeName -y + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("upgrade", $safeName, "-y", "--source", (Get-TrustedChocolateySource)) if ($LASTEXITCODE -eq 0) { Write-Log "Successfully upgraded $safeName" "SUCCESS" } else { diff --git a/src/Choco/choco-utils.ps1 b/src/Choco/choco-utils.ps1 index 0a3e260..56c1fc0 100644 --- a/src/Choco/choco-utils.ps1 +++ b/src/Choco/choco-utils.ps1 @@ -12,10 +12,11 @@ param( . (Join-Path (Resolve-Path (Join-Path $PSScriptRoot "..\..")) "src\Core\core-functions.ps1") if (-not $InputFile) { - $scriptRoot = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } - $InputFile = Join-Path (Resolve-Path (Join-Path $scriptRoot "..\..")) "data\choco_packages.txt" + $InputFile = Get-DefaultPackageListPath } +$InputFile = Resolve-ManagedDataFilePath -Path $InputFile -Purpose "package list" + if ($Action -eq "Update") { if (-not (Test-IsAdmin)) { Write-Log "Elevation required for Update. Re-launching..." "WARN" @@ -27,7 +28,7 @@ if ($Action -eq "Update") { $safeName = Get-ValidatedPackageId -Id $PackageName -Context "Chocolatey" if (-not $safeName) { return } Write-Log "Updating package: $safeName..." "INFO" - choco upgrade $safeName -y + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("upgrade", $safeName, "-y", "--source", (Get-TrustedChocolateySource)) if ($LASTEXITCODE -ne 0) { Write-Log "Failed to update $safeName (Exit Code: $LASTEXITCODE)." "ERROR" } @@ -35,11 +36,20 @@ if ($Action -eq "Update") { else { Write-Log "Updating all packages in $InputFile..." "INFO" $packages = Get-PackageList -Path $InputFile + if ($packages.Count -eq 0) { + Write-Log "No packages found in $InputFile to update." "WARN" + return + } + Write-Host "Trusted Chocolatey source: $(Get-TrustedChocolateySource)" -ForegroundColor DarkGray + if (-not (Read-Confirmation -Prompt "Upgrade $($packages.Count) package(s) from the approved source? Type y to continue" -ExpectedValue "y")) { + Write-Log "Update action cancelled by user." "WARN" + return + } foreach ($p in $packages) { $safeName = Get-ValidatedPackageId -Id $p -Context "Chocolatey" if (-not $safeName) { continue } Write-Log "Upgrading $safeName..." "INFO" - choco upgrade $safeName -y + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("upgrade", $safeName, "-y", "--source", (Get-TrustedChocolateySource)) if ($LASTEXITCODE -ne 0) { Write-Log "Failed to update $safeName (Exit Code: $LASTEXITCODE)." "ERROR" } @@ -56,5 +66,5 @@ elseif ($Action -eq "Info") { $safeName = Get-ValidatedPackageId -Id $PackageName -Context "Chocolatey" if (-not $safeName) { return } Write-Log "Fetching info for $safeName..." "INFO" - choco info $safeName + Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("info", $safeName, "--source", (Get-TrustedChocolateySource)) } diff --git a/src/Choco/list-choco-apps.ps1 b/src/Choco/list-choco-apps.ps1 index 173de20..2f3db50 100644 --- a/src/Choco/list-choco-apps.ps1 +++ b/src/Choco/list-choco-apps.ps1 @@ -7,16 +7,17 @@ param( . (Join-Path (Resolve-Path (Join-Path $PSScriptRoot "..\..")) "src\Core\core-functions.ps1") if (-not $OutputFile) { - $scriptRoot = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } - $OutputFile = Join-Path (Resolve-Path (Join-Path $scriptRoot "..\..")) "data\choco_packages.txt" + $OutputFile = Get-DefaultPackageListPath } +$OutputFile = Resolve-ManagedDataFilePath -Path $OutputFile -Purpose "package export" + Write-Log "Exporting local Chocolatey packages to $OutputFile..." "INFO" try { # Query choco for installed packages # --idonly and --limit-output provide a clean list - $packages = choco list --idonly --limit-output + $packages = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "--idonly", "--limit-output") if ($LASTEXITCODE -ne 0) { throw "Chocolatey command failed with exit code $LASTEXITCODE" @@ -25,6 +26,10 @@ try { $sortedPackages = $packages | Sort-Object | Select-Object -Unique # Save to file + $outputDirectory = Split-Path -Parent $OutputFile + if (-not (Test-Path $outputDirectory)) { + New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null + } $sortedPackages | Out-File -FilePath $OutputFile -Encoding UTF8 -Force Write-Log "Successfully exported $($sortedPackages.Count) packages." "SUCCESS" diff --git a/src/Core/core-functions.ps1 b/src/Core/core-functions.ps1 index 57317f5..2aefadc 100644 --- a/src/Core/core-functions.ps1 +++ b/src/Core/core-functions.ps1 @@ -6,8 +6,242 @@ function Get-ProjectRoot { return $root.Path } +function Get-DataDirectory { + return (Join-Path (Get-ProjectRoot) "data") +} + +function Get-LogDirectory { + return (Join-Path (Get-ProjectRoot) "logs") +} + +function Get-DefaultPackageListPath { + return (Join-Path (Get-DataDirectory) "choco_packages.txt") +} + +function Get-NormalizedPath { + param( + [Parameter(Mandatory=$true)] + [string]$Path + ) + + if ([string]::IsNullOrWhiteSpace($Path)) { + throw "Path is required." + } + + $expandedPath = [Environment]::ExpandEnvironmentVariables($Path) + if (Test-Path $expandedPath) { + return (Resolve-Path $expandedPath).Path + } + + return [System.IO.Path]::GetFullPath($expandedPath) +} + +function Test-PathUnderDirectory { + param( + [Parameter(Mandatory=$true)] + [string]$Path, + [Parameter(Mandatory=$true)] + [string]$Directory + ) + + $normalizedPath = Get-NormalizedPath -Path $Path + $normalizedDirectory = (Get-NormalizedPath -Path $Directory).TrimEnd('\') + $comparison = [System.StringComparison]::OrdinalIgnoreCase + + return $normalizedPath.Equals($normalizedDirectory, $comparison) -or + $normalizedPath.StartsWith("$normalizedDirectory\", $comparison) +} + +function Resolve-ManagedDataFilePath { + param( + [Parameter(Mandatory=$true)] + [string]$Path, + [string]$Purpose = "package list" + ) + + $resolvedPath = Get-NormalizedPath -Path $Path + $dataDirectory = Get-DataDirectory + if (-not (Test-PathUnderDirectory -Path $resolvedPath -Directory $dataDirectory)) { + throw "$Purpose path must remain under '$dataDirectory'." + } + + return $resolvedPath +} + $LogPath = Join-Path (Get-ProjectRoot) "logs\choco-manager.log" +function Get-LogFileLevels { + $configuredLevels = @("WARN", "ERROR") + if ($env:CHOCO_MANAGER_LOG_FILE_LEVELS) { + $parsedLevels = $env:CHOCO_MANAGER_LOG_FILE_LEVELS.Split(',') | ForEach-Object { + $_.Trim().ToUpperInvariant() + } | Where-Object { $_ } + if ($parsedLevels.Count -gt 0) { + $configuredLevels = $parsedLevels + } + } + + return $configuredLevels +} + +function Test-ShouldPersistLogEntry { + param( + [Parameter(Mandatory=$true)] + [string]$Level + ) + + return (Get-LogFileLevels) -contains $Level.ToUpperInvariant() +} + +function Get-TrustedPowerShellPath { + $candidatePaths = @() + if ($env:WINDIR) { + if ($env:PROCESSOR_ARCHITEW6432) { + $candidatePaths += (Join-Path $env:WINDIR "Sysnative\WindowsPowerShell\v1.0\powershell.exe") + } + $candidatePaths += (Join-Path $env:WINDIR "System32\WindowsPowerShell\v1.0\powershell.exe") + } + $candidatePaths += (Join-Path $PSHOME "powershell.exe") + + foreach ($candidatePath in ($candidatePaths | Where-Object { $_ } | Select-Object -Unique)) { + if (Test-Path $candidatePath) { + return (Resolve-Path $candidatePath).Path + } + } + + throw "Unable to locate a trusted Windows PowerShell executable." +} + +function Resolve-TrustedCommandPath { + param( + [Parameter(Mandatory=$true)] + [string]$CommandName + ) + + $name = [System.IO.Path]::GetFileNameWithoutExtension($CommandName).ToLowerInvariant() + switch ($name) { + "powershell" { + return (Get-TrustedPowerShellPath) + } + "choco" { + $command = Get-Command choco -CommandType Application -ErrorAction SilentlyContinue + if (-not $command) { + throw "Chocolatey executable was not found." + } + + $commandPath = Get-NormalizedPath -Path $command.Source + $allowedRoots = @( + $env:ChocolateyInstall, + (Join-Path $env:ProgramData "chocolatey"), + (Join-Path $env:ProgramFiles "Chocolatey") + ) | Where-Object { $_ } | Select-Object -Unique + + foreach ($allowedRoot in $allowedRoots) { + if (Test-PathUnderDirectory -Path $commandPath -Directory $allowedRoot) { + return $commandPath + } + } + + throw "Rejected Chocolatey executable outside trusted directories: $commandPath" + } + "winget" { + $command = Get-Command winget -CommandType Application -ErrorAction SilentlyContinue + if (-not $command) { + throw "Winget executable was not found." + } + + $commandPath = Get-NormalizedPath -Path $command.Source + $allowedRoots = @( + (Join-Path $env:LOCALAPPDATA "Microsoft\WindowsApps"), + (Join-Path $env:ProgramFiles "WindowsApps"), + (Join-Path $env:SystemRoot "System32") + ) | Where-Object { $_ } | Select-Object -Unique + + foreach ($allowedRoot in $allowedRoots) { + if (Test-PathUnderDirectory -Path $commandPath -Directory $allowedRoot) { + return $commandPath + } + } + + throw "Rejected Winget executable outside trusted directories: $commandPath" + } + default { + if (-not [System.IO.Path]::IsPathRooted($CommandName)) { + throw "Executable path must be absolute or use a trusted command alias." + } + + return (Get-NormalizedPath -Path $CommandName) + } + } +} + +function Resolve-ProjectScriptPath { + param( + [Parameter(Mandatory=$true)] + [string]$Path + ) + + $scriptPath = Get-NormalizedPath -Path $Path + if (-not (Test-Path $scriptPath)) { + throw "Script file not found: $scriptPath" + } + + if ([System.IO.Path]::GetExtension($scriptPath) -ne ".ps1") { + throw "Only PowerShell script paths are allowed: $scriptPath" + } + + if (-not (Test-PathUnderDirectory -Path $scriptPath -Directory (Get-ProjectRoot))) { + throw "Script path must remain under the project root: $scriptPath" + } + + return $scriptPath +} + +function Invoke-ScriptFile { + param( + [Parameter(Mandatory=$true)] + [string]$FilePath, + [string[]]$ArgumentList = @() + ) + + $trustedPowerShell = Get-TrustedPowerShellPath + $resolvedScriptPath = Resolve-ProjectScriptPath -Path $FilePath + $processArguments = @("-NoProfile", "-File", $resolvedScriptPath) + $ArgumentList + + Write-Log "Launching script: $resolvedScriptPath" "INFO" + Start-Process -FilePath $trustedPowerShell -ArgumentList $processArguments -Wait -NoNewWindow +} + +function Invoke-TrustedExecutable { + param( + [Parameter(Mandatory=$true)] + [string]$CommandName, + [string[]]$ArgumentList = @() + ) + + $resolvedCommandPath = Resolve-TrustedCommandPath -CommandName $CommandName + return & $resolvedCommandPath @ArgumentList +} + +function Get-TrustedChocolateySource { + return "https://community.chocolatey.org/api/v2/" +} + +function Get-TrustedWingetSource { + return "winget" +} + +function Read-Confirmation { + param( + [Parameter(Mandatory=$true)] + [string]$Prompt, + [string]$ExpectedValue = "y" + ) + + $response = Read-Host $Prompt + return $response -ceq $ExpectedValue +} + function Write-Log { param( [Parameter(Mandatory=$true)] @@ -31,15 +265,17 @@ function Write-Log { Write-Host $logEntry -ForegroundColor $color # File Output - try { - $logDir = Split-Path -Parent $LogPath - if (-not (Test-Path $logDir)) { - New-Item -ItemType Directory -Path $logDir -Force | Out-Null + if (Test-ShouldPersistLogEntry -Level $Level) { + try { + $logDir = Split-Path -Parent $LogPath + if (-not (Test-Path $logDir)) { + New-Item -ItemType Directory -Path $logDir -Force | Out-Null + } + $logEntry | Out-File -FilePath $LogPath -Append -Encoding UTF8 + } + catch { + Write-Warning "Failed to write to log file: $($_.Exception.Message)" } - $logEntry | Out-File -FilePath $LogPath -Append -Encoding UTF8 - } - catch { - Write-Warning "Failed to write to log file: $($_.Exception.Message)" } } @@ -67,7 +303,7 @@ function Show-AuditLog { if (Test-Path $logPath) { Get-Content $logPath -Tail 20 } else { - Write-Host "No log file found." -ForegroundColor Yellow + Write-Host "No persisted log entries found. Adjust CHOCO_MANAGER_LOG_FILE_LEVELS to capture more detail." -ForegroundColor Yellow } } @@ -78,13 +314,17 @@ function Get-ChocoVersionInfo { LatestVersion = $null } - $chocoCmd = Get-Command choco -ErrorAction SilentlyContinue - if (-not $chocoCmd) { return $info } + try { + $null = Resolve-TrustedCommandPath -CommandName "choco" + } + catch { + return $info + } $info.IsInstalled = $true - try { $info.InstalledVersion = (choco --version 2>$null).Trim() } catch { } + try { $info.InstalledVersion = ((Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("--version")) | Select-Object -First 1).Trim() } catch { } try { - $raw = choco list chocolatey --exact -r 2>$null + $raw = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "chocolatey", "--exact", "-r", "--source", (Get-TrustedChocolateySource)) 2>$null foreach ($line in $raw) { if ($line -match '\|') { $parts = $line -split '\|' @@ -101,16 +341,66 @@ function Get-ChocoVersionInfo { function Install-Chocolatey { Write-Host "This will install Chocolatey from community.chocolatey.org." -ForegroundColor Yellow - $confirm = Read-Host "Proceed? (y/n)" - if ($confirm -ne 'y') { return } + Write-Host "The installer will download the Chocolatey package, display its SHA256 hash, and require explicit confirmation before running the local install script." -ForegroundColor Yellow + if (-not (Read-Confirmation -Prompt "Proceed with the secured bootstrap flow? Type y to continue" -ExpectedValue "y")) { return } + + $bootstrapPath = Join-Path $env:TEMP ("choco-manager-bootstrap-{0}.ps1" -f ([Guid]::NewGuid().ToString("N"))) + $bootstrapScript = @' +param( + [Parameter(Mandatory=$true)] + [string]$PackageUrl +) + +$ErrorActionPreference = "Stop" +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072 + +$packageUri = [Uri]$PackageUrl +if ($packageUri.Scheme -ne "https" -or $packageUri.Host -ne "community.chocolatey.org") { + throw "Rejected Chocolatey package URL: $PackageUrl" +} - $command = "& {" + - " Set-ExecutionPolicy Bypass -Scope Process -Force;" + - " [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072;" + - " iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))" + - " }" +$tempRoot = Join-Path $env:TEMP ("choco-manager-install-" + [Guid]::NewGuid().ToString("N")) +$packagePath = Join-Path $tempRoot "chocolatey.nupkg" +$extractPath = Join-Path $tempRoot "package" + +try { + New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null + Invoke-WebRequest -Uri $packageUri.AbsoluteUri -OutFile $packagePath -UseBasicParsing + $hash = (Get-FileHash -Path $packagePath -Algorithm SHA256).Hash + Write-Host "Downloaded Chocolatey package hash (SHA256): $hash" -ForegroundColor Yellow + $approval = Read-Host "Type INSTALL to execute the local Chocolatey package installer" + if ($approval -cne "INSTALL") { + throw "Chocolatey installation cancelled by user." + } - Invoke-ElevatedProcess -FilePath "powershell.exe" -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", $command) + Expand-Archive -Path $packagePath -DestinationPath $extractPath -Force + $installScript = Get-ChildItem -Path $extractPath -Recurse -Filter "chocolateyInstall.ps1" | Select-Object -First 1 -ExpandProperty FullName + if (-not $installScript) { + throw "Could not locate chocolateyInstall.ps1 inside the Chocolatey package." + } + + Unblock-File -Path $installScript -ErrorAction SilentlyContinue + & $installScript + if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { + throw "Chocolatey installer exited with code $LASTEXITCODE." + } +} +finally { + Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path $PSCommandPath -Force -ErrorAction SilentlyContinue +} +'@ + + Set-Content -Path $bootstrapPath -Value $bootstrapScript -Encoding UTF8 + + try { + $trustedPowerShell = Get-TrustedPowerShellPath + Invoke-ElevatedProcess -FilePath $trustedPowerShell -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $bootstrapPath, "-PackageUrl", "https://community.chocolatey.org/api/v2/package/chocolatey") + } + catch { + Remove-Item -Path $bootstrapPath -Force -ErrorAction SilentlyContinue + throw + } } function Test-IsAdmin { @@ -138,16 +428,18 @@ function Invoke-ElevatedAction { [string[]]$ArgumentList = @() ) - $args = @("-ExecutionPolicy", "Bypass", "-File", $FilePath) + $ArgumentList + $trustedPowerShell = Get-TrustedPowerShellPath + $resolvedScriptPath = Resolve-ProjectScriptPath -Path $FilePath + $args = @("-NoProfile", "-File", $resolvedScriptPath) + $ArgumentList if (Test-IsAdmin) { - Write-Log "Running as Administrator: $FilePath $($ArgumentList -join ' ')" "INFO" - Start-Process -FilePath "powershell.exe" -ArgumentList $args -Wait -NoNewWindow + Write-Log "Running as Administrator: $resolvedScriptPath $($ArgumentList -join ' ')" "INFO" + Start-Process -FilePath $trustedPowerShell -ArgumentList $args -Wait -NoNewWindow } else { - Write-Log "Requesting Elevation for: $FilePath" "WARN" + Write-Log "Requesting Elevation for: $resolvedScriptPath" "WARN" try { - Start-Process -FilePath "powershell.exe" -ArgumentList $args -Verb RunAs -Wait + Start-Process -FilePath $trustedPowerShell -ArgumentList $args -Verb RunAs -Wait Write-Log "Elevated process completed." "SUCCESS" } catch { @@ -164,14 +456,16 @@ function Invoke-ElevatedProcess { [string[]]$ArgumentList = @() ) + $resolvedFilePath = Resolve-TrustedCommandPath -CommandName $FilePath + if (Test-IsAdmin) { - Write-Log "Running as Administrator: $FilePath $($ArgumentList -join ' ')" "INFO" - Start-Process -FilePath $FilePath -ArgumentList $ArgumentList -Wait -NoNewWindow + Write-Log "Running as Administrator: $resolvedFilePath $($ArgumentList -join ' ')" "INFO" + Start-Process -FilePath $resolvedFilePath -ArgumentList $ArgumentList -Wait -NoNewWindow } else { - Write-Log "Requesting Elevation for: $FilePath" "WARN" + Write-Log "Requesting Elevation for: $resolvedFilePath" "WARN" try { - Start-Process -FilePath $FilePath -ArgumentList $ArgumentList -Verb RunAs -Wait + Start-Process -FilePath $resolvedFilePath -ArgumentList $ArgumentList -Verb RunAs -Wait Write-Log "Elevated process completed." "SUCCESS" } catch { diff --git a/src/Winget/winget-utils.ps1 b/src/Winget/winget-utils.ps1 index 0cff9b9..1a03620 100644 --- a/src/Winget/winget-utils.ps1 +++ b/src/Winget/winget-utils.ps1 @@ -11,8 +11,13 @@ param( . (Join-Path (Resolve-Path (Join-Path $PSScriptRoot "..\..")) "src\Core\core-functions.ps1") function Test-Winget { - $winget = Get-Command winget -ErrorAction SilentlyContinue - return $null -ne $winget + try { + $null = Resolve-TrustedCommandPath -CommandName "winget" + return $true + } + catch { + return $false + } } if (-not (Test-Winget)) { @@ -22,7 +27,7 @@ if (-not (Test-Winget)) { function Invoke-WingetList { Write-Log "Fetching Winget packages..." "INFO" - $raw = winget list + $raw = Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("list") if ($LASTEXITCODE -ne 0) { Write-Log "Winget list failed (Exit Code: $LASTEXITCODE)." "ERROR" return @@ -66,7 +71,7 @@ function Invoke-WingetList { $safeId = Get-ValidatedPackageId -Id $pkgId -Context "Winget" if ($safeId) { Write-Host "`n--- Winget Info for $safeId ---" -ForegroundColor Yellow - winget show --id $safeId + Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("show", "--id", $safeId, "--exact", "--source", (Get-TrustedWingetSource)) Pause } } @@ -77,8 +82,14 @@ function Invoke-WingetInstall { $safeId = Get-ValidatedPackageId -Id $Id -Context "Winget" if (-not $safeId) { return } + Write-Host "Trusted Winget source: $(Get-TrustedWingetSource)" -ForegroundColor DarkGray + if (-not (Read-Confirmation -Prompt "Install '$safeId' from the approved Winget source? Type y to continue" -ExpectedValue "y")) { + Write-Log "Winget install cancelled by user." "WARN" + return + } + Write-Log "Installing '$safeId' via Winget..." "INFO" - winget install --id $safeId --silent --accept-package-agreements --accept-source-agreements + Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("install", "--id", $safeId, "--exact", "--source", (Get-TrustedWingetSource), "--silent", "--accept-package-agreements", "--accept-source-agreements") if ($LASTEXITCODE -eq 0) { Write-Log "Successfully installed $safeId via Winget." "SUCCESS" } @@ -93,7 +104,7 @@ function Invoke-WingetInfo { if (-not $safeId) { return } Write-Log "Fetching Winget info for '$safeId'..." "INFO" - winget show --id $safeId + Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("show", "--id", $safeId, "--exact", "--source", (Get-TrustedWingetSource)) if ($LASTEXITCODE -ne 0) { Write-Log "Winget info failed for $safeId (Exit Code: $LASTEXITCODE)." "ERROR" } @@ -104,8 +115,13 @@ function Invoke-WingetRemove { $safeId = Get-ValidatedPackageId -Id $Id -Context "Winget" if (-not $safeId) { return } + if (-not (Read-Confirmation -Prompt "Remove '$safeId'? Type y to continue" -ExpectedValue "y")) { + Write-Log "Winget remove cancelled by user." "WARN" + return + } + Write-Log "Uninstalling '$safeId' via Winget..." "INFO" - winget uninstall --id $safeId --silent --accept-package-agreements --accept-source-agreements + Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("uninstall", "--id", $safeId, "--exact", "--silent", "--accept-package-agreements", "--accept-source-agreements") if ($LASTEXITCODE -eq 0) { Write-Log "Successfully uninstalled $safeId via Winget." "SUCCESS" } @@ -125,7 +141,7 @@ function Invoke-WingetSearchInteractive { $term = $term.Trim() Write-Log "Searching Winget for '$term'..." "INFO" - $raw = winget search $term + $raw = Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("search", $term, "--source", (Get-TrustedWingetSource)) if ($LASTEXITCODE -ne 0) { Write-Log "Winget search failed (Exit Code: $LASTEXITCODE)." "ERROR" return diff --git a/winget-utils.ps1 b/winget-utils.ps1 index 734e861..778dcbf 100644 --- a/winget-utils.ps1 +++ b/winget-utils.ps1 @@ -1,2 +1,2 @@ # Wrapper script for winget-utils -powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "src\Winget\winget-utils.ps1") @args +& (Join-Path $PSScriptRoot "src\Winget\winget-utils.ps1") @args From 5bd9285facd950e83603508e1f731dbf693f3a49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:59:25 +0000 Subject: [PATCH 2/6] Finalize validation for security hardening changes Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com> --- src/Core/core-functions.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Core/core-functions.ps1 b/src/Core/core-functions.ps1 index 2aefadc..aaa8b12 100644 --- a/src/Core/core-functions.ps1 +++ b/src/Core/core-functions.ps1 @@ -348,7 +348,9 @@ function Install-Chocolatey { $bootstrapScript = @' param( [Parameter(Mandatory=$true)] - [string]$PackageUrl + [string]$PackageUrl, + [Parameter(Mandatory=$true)] + [string]$BootstrapScriptPath ) $ErrorActionPreference = "Stop" @@ -387,7 +389,7 @@ try { } finally { Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path $PSCommandPath -Force -ErrorAction SilentlyContinue + Remove-Item -Path $BootstrapScriptPath -Force -ErrorAction SilentlyContinue } '@ @@ -395,7 +397,7 @@ finally { try { $trustedPowerShell = Get-TrustedPowerShellPath - Invoke-ElevatedProcess -FilePath $trustedPowerShell -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $bootstrapPath, "-PackageUrl", "https://community.chocolatey.org/api/v2/package/chocolatey") + Invoke-ElevatedProcess -FilePath $trustedPowerShell -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $bootstrapPath, "-PackageUrl", "https://community.chocolatey.org/api/v2/package/chocolatey", "-BootstrapScriptPath", $bootstrapPath) } catch { Remove-Item -Path $bootstrapPath -Force -ErrorAction SilentlyContinue From c6ef82f4f1742d0677e2c1eece37529896e4aaa1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:00:43 +0000 Subject: [PATCH 3/6] Address validation feedback for hardening changes Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com> --- src/Choco/choco-pack-install.ps1 | 8 ++++++- src/Core/core-functions.ps1 | 39 ++++++++++++++++++++++++++++---- src/Winget/winget-utils.ps1 | 5 ++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/Choco/choco-pack-install.ps1 b/src/Choco/choco-pack-install.ps1 index 8437a45..896a947 100644 --- a/src/Choco/choco-pack-install.ps1 +++ b/src/Choco/choco-pack-install.ps1 @@ -28,6 +28,12 @@ if (-not $SkipUpgradeChoco) { # Get currently installed packages for comparison $installedPackages = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "--local-only", "--limit-output") | ForEach-Object { $_.Split('|')[0] } +$installedSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) +foreach ($installedPackage in $installedPackages) { + if (-not [string]::IsNullOrWhiteSpace($installedPackage)) { + $null = $installedSet.Add($installedPackage) + } +} # Read target packages using core helper $targetPackages = Get-PackageList -Path $InputFile @@ -37,7 +43,7 @@ if ($targetPackages.Count -eq 0) { exit } -$missingPackages = $targetPackages | Where-Object { $installedPackages -notcontains $_ } +$missingPackages = $targetPackages | Where-Object { -not $installedSet.Contains($_) } if ($missingPackages.Count -eq 0) { Write-Log "All packages from $InputFile are already installed." "SUCCESS" exit diff --git a/src/Core/core-functions.ps1 b/src/Core/core-functions.ps1 index aaa8b12..708ca90 100644 --- a/src/Core/core-functions.ps1 +++ b/src/Core/core-functions.ps1 @@ -231,6 +231,21 @@ function Get-TrustedWingetSource { return "winget" } +function Get-SecureBootstrapDirectory { + $baseDirectory = if ($env:LOCALAPPDATA) { + Join-Path $env:LOCALAPPDATA "Choco-Manager\bootstrap" + } + else { + Join-Path (Get-ProjectRoot) "logs\bootstrap" + } + + if (-not (Test-Path $baseDirectory)) { + New-Item -ItemType Directory -Path $baseDirectory -Force | Out-Null + } + + return (Get-NormalizedPath -Path $baseDirectory) +} + function Read-Confirmation { param( [Parameter(Mandatory=$true)] @@ -322,7 +337,12 @@ function Get-ChocoVersionInfo { } $info.IsInstalled = $true - try { $info.InstalledVersion = ((Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("--version")) | Select-Object -First 1).Trim() } catch { } + try { + $info.InstalledVersion = ((Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("--version")) | Select-Object -First 1).Trim() + } + catch { + Write-Log "Unable to determine the installed Chocolatey version: $($_.Exception.Message)" "WARN" + } try { $raw = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "chocolatey", "--exact", "-r", "--source", (Get-TrustedChocolateySource)) 2>$null foreach ($line in $raw) { @@ -344,7 +364,8 @@ function Install-Chocolatey { Write-Host "The installer will download the Chocolatey package, display its SHA256 hash, and require explicit confirmation before running the local install script." -ForegroundColor Yellow if (-not (Read-Confirmation -Prompt "Proceed with the secured bootstrap flow? Type y to continue" -ExpectedValue "y")) { return } - $bootstrapPath = Join-Path $env:TEMP ("choco-manager-bootstrap-{0}.ps1" -f ([Guid]::NewGuid().ToString("N"))) + $bootstrapDirectory = Get-SecureBootstrapDirectory + $bootstrapPath = Join-Path $bootstrapDirectory ("bootstrap-{0}.ps1" -f ([Guid]::NewGuid().ToString("N"))) $bootstrapScript = @' param( [Parameter(Mandatory=$true)] @@ -361,7 +382,12 @@ if ($packageUri.Scheme -ne "https" -or $packageUri.Host -ne "community.chocolate throw "Rejected Chocolatey package URL: $PackageUrl" } -$tempRoot = Join-Path $env:TEMP ("choco-manager-install-" + [Guid]::NewGuid().ToString("N")) +$bootstrapRoot = if ($env:LOCALAPPDATA) { + Join-Path $env:LOCALAPPDATA "Choco-Manager\bootstrap" +} else { + Join-Path $env:TEMP "Choco-Manager-bootstrap" +} +$tempRoot = Join-Path $bootstrapRoot ("install-" + [Guid]::NewGuid().ToString("N")) $packagePath = Join-Path $tempRoot "chocolatey.nupkg" $extractPath = Join-Path $tempRoot "package" @@ -381,7 +407,12 @@ try { throw "Could not locate chocolateyInstall.ps1 inside the Chocolatey package." } - Unblock-File -Path $installScript -ErrorAction SilentlyContinue + try { + Unblock-File -Path $installScript -ErrorAction Stop + } + catch { + Write-Warning "Unable to remove the downloaded file zone marker from $installScript: $($_.Exception.Message)" + } & $installScript if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { throw "Chocolatey installer exited with code $LASTEXITCODE." diff --git a/src/Winget/winget-utils.ps1 b/src/Winget/winget-utils.ps1 index 1a03620..cfdbf0d 100644 --- a/src/Winget/winget-utils.ps1 +++ b/src/Winget/winget-utils.ps1 @@ -82,8 +82,9 @@ function Invoke-WingetInstall { $safeId = Get-ValidatedPackageId -Id $Id -Context "Winget" if (-not $safeId) { return } - Write-Host "Trusted Winget source: $(Get-TrustedWingetSource)" -ForegroundColor DarkGray - if (-not (Read-Confirmation -Prompt "Install '$safeId' from the approved Winget source? Type y to continue" -ExpectedValue "y")) { + $trustedSource = Get-TrustedWingetSource + Write-Host "Trusted Winget source: $trustedSource" -ForegroundColor DarkGray + if (-not (Read-Confirmation -Prompt "Install '$safeId' from the approved Winget source '$trustedSource'? Type y to continue" -ExpectedValue "y")) { Write-Log "Winget install cancelled by user." "WARN" return } From eeb716ea73f4e3ed9e6f764768b135a9aaafdb18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:01:49 +0000 Subject: [PATCH 4/6] Tidy bootstrap cleanup handling Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com> --- src/Core/core-functions.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Core/core-functions.ps1 b/src/Core/core-functions.ps1 index 708ca90..81ab808 100644 --- a/src/Core/core-functions.ps1 +++ b/src/Core/core-functions.ps1 @@ -371,7 +371,7 @@ param( [Parameter(Mandatory=$true)] [string]$PackageUrl, [Parameter(Mandatory=$true)] - [string]$BootstrapScriptPath + [string]$SelfPath ) $ErrorActionPreference = "Stop" @@ -414,13 +414,13 @@ try { Write-Warning "Unable to remove the downloaded file zone marker from $installScript: $($_.Exception.Message)" } & $installScript - if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { + if ($LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0) { throw "Chocolatey installer exited with code $LASTEXITCODE." } } finally { Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path $BootstrapScriptPath -Force -ErrorAction SilentlyContinue + Remove-Item -Path $SelfPath -Force -ErrorAction SilentlyContinue } '@ @@ -428,7 +428,7 @@ finally { try { $trustedPowerShell = Get-TrustedPowerShellPath - Invoke-ElevatedProcess -FilePath $trustedPowerShell -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $bootstrapPath, "-PackageUrl", "https://community.chocolatey.org/api/v2/package/chocolatey", "-BootstrapScriptPath", $bootstrapPath) + Invoke-ElevatedProcess -FilePath $trustedPowerShell -ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $bootstrapPath, "-PackageUrl", "https://community.chocolatey.org/api/v2/package/chocolatey", "-SelfPath", $bootstrapPath) } catch { Remove-Item -Path $bootstrapPath -Force -ErrorAction SilentlyContinue From 5ea80fc5fdea853fd2295709037d44dc3899e60e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:02:49 +0000 Subject: [PATCH 5/6] Clean up bootstrap and winget search handling Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com> --- src/Choco/choco-package-explorer.ps1 | 5 +++-- src/Core/core-functions.ps1 | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Choco/choco-package-explorer.ps1 b/src/Choco/choco-package-explorer.ps1 index 630453d..0869d78 100644 --- a/src/Choco/choco-package-explorer.ps1 +++ b/src/Choco/choco-package-explorer.ps1 @@ -215,8 +215,9 @@ function Search-Packages { } } else { # Winget Search - $raw = Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("search", $keyword, "--source", (Get-TrustedWingetSource)) - $raw + Invoke-TrustedExecutable -CommandName "winget" -ArgumentList @("search", $keyword, "--source", (Get-TrustedWingetSource)) | ForEach-Object { + Write-Host $_ + } $id = Read-Host "`nEnter Package ID for more info (or press Enter to skip)" if ($id) { $safeId = Get-ValidatedPackageId -Id $id -Context "Winget" diff --git a/src/Core/core-functions.ps1 b/src/Core/core-functions.ps1 index 81ab808..c221b75 100644 --- a/src/Core/core-functions.ps1 +++ b/src/Core/core-functions.ps1 @@ -414,7 +414,7 @@ try { Write-Warning "Unable to remove the downloaded file zone marker from $installScript: $($_.Exception.Message)" } & $installScript - if ($LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0) { + if ($LASTEXITCODE -ne 0) { throw "Chocolatey installer exited with code $LASTEXITCODE." } } From efbb5d57e2ba494dbf45e398259d86ff050b59cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:03:39 +0000 Subject: [PATCH 6/6] Align log helper and package set naming Agent-Logs-Url: https://github.com/arrfour/choco-manager/sessions/987a926a-0677-4597-b12d-5dc3fc407641 Co-authored-by: arrfour <28449367+arrfour@users.noreply.github.com> --- src/Choco/choco-pack-install.ps1 | 6 +++--- src/Core/core-functions.ps1 | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Choco/choco-pack-install.ps1 b/src/Choco/choco-pack-install.ps1 index 896a947..a8f9303 100644 --- a/src/Choco/choco-pack-install.ps1 +++ b/src/Choco/choco-pack-install.ps1 @@ -28,10 +28,10 @@ if (-not $SkipUpgradeChoco) { # Get currently installed packages for comparison $installedPackages = Invoke-TrustedExecutable -CommandName "choco" -ArgumentList @("list", "--local-only", "--limit-output") | ForEach-Object { $_.Split('|')[0] } -$installedSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) +$installedPackageSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($installedPackage in $installedPackages) { if (-not [string]::IsNullOrWhiteSpace($installedPackage)) { - $null = $installedSet.Add($installedPackage) + $null = $installedPackageSet.Add($installedPackage) } } @@ -43,7 +43,7 @@ if ($targetPackages.Count -eq 0) { exit } -$missingPackages = $targetPackages | Where-Object { -not $installedSet.Contains($_) } +$missingPackages = $targetPackages | Where-Object { -not $installedPackageSet.Contains($_) } if ($missingPackages.Count -eq 0) { Write-Log "All packages from $InputFile are already installed." "SUCCESS" exit diff --git a/src/Core/core-functions.ps1 b/src/Core/core-functions.ps1 index c221b75..3dbd4a2 100644 --- a/src/Core/core-functions.ps1 +++ b/src/Core/core-functions.ps1 @@ -84,7 +84,7 @@ function Get-LogFileLevels { return $configuredLevels } -function Test-ShouldPersistLogEntry { +function Test-LogLevelPersistence { param( [Parameter(Mandatory=$true)] [string]$Level @@ -280,7 +280,7 @@ function Write-Log { Write-Host $logEntry -ForegroundColor $color # File Output - if (Test-ShouldPersistLogEntry -Level $Level) { + if (Test-LogLevelPersistence -Level $Level) { try { $logDir = Split-Path -Parent $LogPath if (-not (Test-Path $logDir)) {