-
Notifications
You must be signed in to change notification settings - Fork 1
Fixed build issues & view loading #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
📝 WalkthroughWalkthroughUpdates version handling in CI/CD workflows to separate numeric/build components. Refactors GUI views with thread-safe async UI update mechanisms via new Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant WingetModule as Winget Module
participant PowerShell
participant WingetAPI as Winget API
participant WingetCLI as Winget CLI
Client->>WingetModule: search_packages()
activate WingetModule
WingetModule->>PowerShell: _ensure_winget_module()
alt Microsoft.WinGet.Client Available
PowerShell-->>WingetModule: Module Ready
WingetModule->>PowerShell: _search_via_powershell()
alt PowerShell Search Success
PowerShell-->>WingetModule: JSON Results
WingetModule-->>Client: Package List
else PowerShell Search Fails
WingetModule->>WingetAPI: Fallback to API
WingetAPI-->>WingetModule: Results
WingetModule-->>Client: Package List
end
else Module Not Available
PowerShell-->>WingetModule: Install Attempt
alt Module Install Succeeds
WingetModule->>PowerShell: Retry Search
PowerShell-->>WingetModule: Results
else Module Install Fails
WingetModule->>WingetCLI: Fallback to CLI
WingetCLI-->>WingetModule: CLI Output
end
WingetModule-->>Client: Results
end
deactivate WingetModule
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/switchcraft_winget/utils/winget.py (3)
130-147: Command injection vulnerability via unsanitized query input.The
queryparameter is interpolated directly into a PowerShell command string without sanitization. If a user searches for a query containing single quotes or PowerShell escape sequences (e.g.,test'; Remove-Item -Recurse C:\; '), it could execute arbitrary commands.Consider using parameterized PowerShell input or sanitizing the query:
🔒 Proposed fix using sanitization
def _search_via_powershell(self, query: str) -> List[Dict[str, str]]: ... try: # Ensure module is available (but don't fail if it's not - we have fallbacks) self._ensure_winget_module() - - ps_script = f"Import-Module Microsoft.WinGet.Client -ErrorAction SilentlyContinue; Find-WinGetPackage -Query '{query}' | Select-Object Name, Id, Version, Source | ConvertTo-Json -Depth 1" + + # Sanitize query to prevent command injection + safe_query = query.replace("'", "''").replace("`", "``") + ps_script = f"Import-Module Microsoft.WinGet.Client -ErrorAction SilentlyContinue; Find-WinGetPackage -Query '{safe_query}' | Select-Object Name, Id, Version, Source | ConvertTo-Json -Depth 1"
352-367: CLI install fallback missing timeout.The PowerShell install path has a 300-second timeout (line 658), but the CLI fallback
subprocess.runon line 363 has no timeout. A stuck installation could hang the application indefinitely.🐛 Proposed fix
- proc = subprocess.run(cmd, capture_output=True, text=True, **kwargs) + proc = subprocess.run(cmd, capture_output=True, text=True, timeout=300, **kwargs) if proc.returncode != 0: logger.error(f"Winget install failed: {proc.stderr}") return False return True + except subprocess.TimeoutExpired: + logger.error(f"Winget CLI install timed out for package: {package_id}") + return False except Exception as e:
384-398: CLI download fallback missing timeout.Same issue: the
subprocess.runon line 395 has no timeout, potentially causing the application to hang if the download stalls.🐛 Proposed fix
- proc = subprocess.run(cmd, capture_output=True, text=True, **kwargs) + proc = subprocess.run(cmd, capture_output=True, text=True, timeout=300, **kwargs) if proc.returncode != 0:Add timeout exception handling similar to
get_package_details.src/switchcraft/gui_modern/views/settings_view.py (1)
1749-1856: GPO cert path is detected but not honoredIf policy sets
CodeSigningCertPath(without a thumbprint), the method still proceeds to auto-detect and writes user prefs. That can surface a non‑policy cert in UI and store conflicting preferences. Consider short‑circuiting when a GPO path exists and skipping preference writes whenever either GPO value is set.✅ Suggested fix
- gpo_thumb = SwitchCraftConfig.get_value("CodeSigningCertThumbprint", "") - gpo_cert_path = SwitchCraftConfig.get_value("CodeSigningCertPath", "") + gpo_thumb = SwitchCraftConfig.get_value("CodeSigningCertThumbprint", "") + gpo_cert_path = SwitchCraftConfig.get_value("CodeSigningCertPath", "") + import os ... - if gpo_thumb or gpo_cert_path: + if gpo_thumb or gpo_cert_path: try: if gpo_thumb: ... if verify_proc.returncode == 0 and "FOUND" in verify_proc.stdout: ... return + if gpo_cert_path: + if os.path.exists(gpo_cert_path): + self.cert_status_text.value = f"GPO: {gpo_cert_path}" + self.cert_status_text.color = "GREEN" + self.update() + self._show_snack(i18n.get("cert_gpo_detected") or "GPO-configured certificate detected.", "GREEN") + return except Exception as ex: logger.debug(f"GPO cert verification failed: {ex}") # Continue with auto-detection if verification fails ... - if not gpo_thumb: + if not (gpo_thumb or gpo_cert_path): SwitchCraftConfig.set_user_preference("CodeSigningCertThumbprint", thumb) SwitchCraftConfig.set_user_preference("CodeSigningCertPath", "") ... - if not gpo_thumb: + if not (gpo_thumb or gpo_cert_path): SwitchCraftConfig.set_user_preference("CodeSigningCertThumbprint", thumb)
🤖 Fix all issues with AI agents
In @.github/workflows/release.yml:
- Around line 188-200: The sed replacements that set MyAppVersion,
MyAppVersionNumeric, and MyAppVersionInfo in both switchcraft_modern.iss and
switchcraft_legacy.iss should preserve any leading indentation; update each sed
invocation that currently matches "#define MyAppVersion", "#define
MyAppVersionNumeric" and "#define MyAppVersionInfo" to capture and reuse leading
whitespace (e.g., a capture for ^[[:space:]]*) so the replacement reinserts the
same indentation while substituting DEV_VERSION, BASE_VERSION and VERSION_INFO
respectively for the six occurrences in the diff.
In `@src/switchcraft_winget/utils/winget.py`:
- Around line 664-702: The _download_via_powershell function is misleading
because it never downloads and always returns None; either remove the function
or rename it (e.g., _verify_package_exists_via_powershell) and adjust callers
(like download_package) accordingly so behavior is clear; if you keep the
existence check, ensure it actually affects control flow (return a boolean/raise
on not found) and avoid command injection by not interpolating package_id into
the PowerShell string—pass it as a safe argument or validate/escape it before
use, and update any logic that expects a Path (e.g., callers of
_download_via_powershell) to handle the new return type.
- Around line 536-573: In _ensure_winget_module, stop silently forcing an
install: add a configuration flag (e.g. self.auto_install_winget or a param) and
only attempt the Install-Module block when that flag is True; if the flag is
False, skip running the ps_script install branch and immediately return False so
the CLI fallback is used. If you keep auto-installation enabled, log an info
message before attempting installation (logger.info(...)) and after a successful
install (detect "INSTALLED" in proc.stdout and logger.info("Automatically
installed Microsoft.WinGet.Client")). Update the function to reference the
existing ps_script, subprocess.run call and proc.stdout/proc.returncode checks
to implement the opt-in behavior and additional logs.
- Around line 582-588: The PowerShell snippet in ps_script interpolates
package_id directly (vulnerable to command injection), so change the script to
accept a parameter (e.g., param($id)) and reference $id inside the script
instead of inserting package_id into the string, and invoke the PowerShell
process with an ArgumentList (or equivalent API) to pass package_id as a
separate argument; do the same pattern used in _search_via_powershell (or adapt
that secure approach) so package_id is not concatenated into ps_script.
- Around line 637-646: The PS script builds a command using the unsanitized
package_id (ps_script / Install-WinGetPackage) leading to command injection;
sanitize or validate package_id the same way you handled other inputs (e.g.,
escape/whitelist or use a safe helper function used elsewhere) before
interpolating it into ps_script, and ensure the sanitized value is used in the
Install-WinGetPackage -Id parameter and any related string construction.
In `@src/switchcraft/gui_modern/views/winget_view.py`:
- Around line 372-384: The code merges short_info and full without checking
full, which can be None and cause a TypeError; update the block after calling
self.winget.get_package_details(short_info['Id']) (the get_package_details call)
to validate/coerce full (e.g., if full is None: log a warning and set full = {}
or raise immediately) before running merged = {**short_info, **full}, then set
self.current_pkg = merged and keep the existing logger messages; ensure the
existing check that raises on empty details still runs after the validation or
replace it with the immediate None-handling logic.
🧹 Nitpick comments (7)
scripts/build_release.ps1 (1)
157-161: Consider extracting version parsing into a helper function.The version extraction logic (regex matching, numeric extraction, and
AppVersionInfoconstruction) is duplicated between the fallback block (lines 157-161) and the successful extraction block (lines 168-173). While functional, this creates a maintenance burden if the parsing logic needs to change.♻️ Suggested refactor
+# Helper function to extract version components +function Get-VersionComponents { + param([string]$Version) + $Numeric = if ($Version -match '^(\d+\.\d+\.\d+)') { $Matches[1] } else { $Version -replace '[^0-9.].*', '' } + return @{ + Full = $Version + Numeric = $Numeric + Info = "$Numeric.0" + } +} + # --- Version Extraction --- $PyProjectFile = Join-Path $RepoRoot "pyproject.toml" # Fallback version if extraction fails (can be overridden via env variable) $AppVersion = if ($env:SWITCHCRAFT_VERSION) { $env:SWITCHCRAFT_VERSION } else { "2026.1.2" } -# Extract numeric version only (remove .dev0, +build, -dev, etc.) for VersionInfoVersion -# Pattern: extract MAJOR.MINOR.PATCH from any version format -$AppVersionNumeric = if ($AppVersion -match '^(\d+\.\d+\.\d+)') { $Matches[1] } else { $AppVersion -replace '[^0-9.].*', '' } -# VersionInfoVersion requires 4 numeric components (Major.Minor.Patch.Build) -$AppVersionInfo = "$AppVersionNumeric.0" +$VersionParts = Get-VersionComponents -Version $AppVersion +$AppVersionNumeric = $VersionParts.Numeric +$AppVersionInfo = $VersionParts.Info if (Test-Path $PyProjectFile) { try { $VersionLine = Get-Content -Path $PyProjectFile | Select-String "version = " | Select-Object -First 1 if ($VersionLine -match 'version = "(.*)"') { $AppVersion = $Matches[1] - # Extract numeric version only (remove .dev0, +build, -dev, etc.) for VersionInfoVersion - # Pattern: extract MAJOR.MINOR.PATCH from any version format - $AppVersionNumeric = if ($AppVersion -match '^(\d+\.\d+\.\d+)') { $Matches[1] } else { $AppVersion -replace '[^0-9.].*', '' } - # VersionInfoVersion requires 4 numeric components (Major.Minor.Patch.Build) - $AppVersionInfo = "$AppVersionNumeric.0" + $VersionParts = Get-VersionComponents -Version $AppVersion + $AppVersionNumeric = $VersionParts.Numeric + $AppVersionInfo = $VersionParts.Info Write-Host "Detected Version: $AppVersion (Numeric base: $AppVersionNumeric, Info: $AppVersionInfo)" -ForegroundColor CyanAlso applies to: 168-173
src/switchcraft_winget/utils/winget.py (1)
136-146: Consider extracting repeated subprocess kwargs setup into a helper.The pattern for building subprocess kwargs with
startupinfoandcreationflagsis duplicated ~8 times throughout this file. Additionally,import sysappears inside multiple functions.♻️ Suggested helper method
def _get_subprocess_kwargs(self) -> dict: """Build subprocess kwargs for hidden console window on Windows.""" kwargs = {} startupinfo = self._get_startup_info() if startupinfo: kwargs['startupinfo'] = startupinfo if sys.platform == "win32": kwargs['creationflags'] = getattr(subprocess, 'CREATE_NO_WINDOW', 0x08000000) return kwargsThen replace all instances with:
kwargs = self._get_subprocess_kwargs() proc = subprocess.run(cmd, capture_output=True, text=True, ..., **kwargs)Also move
import systo the module level (line 1-8 area).src/switchcraft/gui_modern/utils/view_utils.py (1)
57-99: Add a direct-call fallback whenrun_taskis unavailable.At Line 79-81,
_run_task_safereturnsFalsewithout executingfuncifpageorrun_taskis missing, which can silently skip UI updates (e.g., tests or older Flet builds). Consider falling back to a direct call to preserve behavior.💡 Suggested change
- if not page or not hasattr(page, 'run_task'): - return False + if not page or not hasattr(page, 'run_task'): + try: + func() + return True + except Exception as ex: + logger.debug(f"Failed to run task directly: {ex}") + return Falsesrc/switchcraft/gui_modern/views/winget_view.py (1)
463-488: Consider delegating toViewMixin._run_task_safefor consistencyThis logic mirrors the shared helper and is likely to drift over time. Centralizing would keep UI marshaling consistent across views.
♻️ Possible direction
- import inspect - import asyncio - - # Check if function is async - is_async = inspect.iscoroutinefunction(ui_func) - - if hasattr(self, 'app_page') and hasattr(self.app_page, 'run_task'): - try: - if is_async: - self.app_page.run_task(ui_func) - logger.debug("UI update scheduled via run_task (async)") - else: - async def async_wrapper(): - ui_func() - self.app_page.run_task(async_wrapper) - logger.debug("UI update scheduled via run_task (sync wrapped)") - except Exception as ex: - ... - else: - ... + # Reuse shared helper to keep behavior consistent across views + if not self._run_task_safe(ui_func): + logger.debug("UI update executed directly (no run_task available)")src/switchcraft/gui_modern/views/intune_store_view.py (1)
214-227: Consolidate run_task wrapping viaViewMixin._run_task_safeThis wrapper pattern is repeated across several call sites. Using the shared helper would reduce duplication and keep marshaling behavior consistent across views.
Also applies to: 315-322, 384-387, 607-613
src/switchcraft/gui_modern/views/group_manager_view.py (1)
489-491: Redundanthasattr(run_task)guard around_run_task_safe
_run_task_safealready handles the absence ofrun_taskand falls back; you can call it unconditionally here.src/switchcraft/gui_modern/views/settings_view.py (1)
1521-1554: Extract addon asset selection into a shared helperThe asset-matching logic is now duplicated in two functions. A small helper (e.g.,
_select_addon_asset(assets, addon_id)) would reduce drift and keep the search rules consistent.Also applies to: 1594-1617, 1645-1649
| # Update .iss files (Inno Setup installer scripts) | ||
| # Extract numeric version only (remove .dev0, +build, etc.) for VersionInfoVersion | ||
| BASE_VERSION=$(echo "$DEV_VERSION" | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/') | ||
| # VersionInfoVersion requires 4 numeric components (Major.Minor.Patch.Build) | ||
| VERSION_INFO="${BASE_VERSION}.0" | ||
| # For dev releases: MyAppVersion should include commit ID (full DEV_VERSION) | ||
| # MyAppVersionNumeric should be numeric only (BASE_VERSION) | ||
| sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$DEV_VERSION\"/" switchcraft_modern.iss | ||
| sed -i "s/#define MyAppVersionNumeric \".*\"/#define MyAppVersionNumeric \"$BASE_VERSION\"/" switchcraft_modern.iss | ||
| sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$BASE_VERSION\"/" switchcraft_legacy.iss | ||
| sed -i "s/#define MyAppVersionInfo \".*\"/#define MyAppVersionInfo \"$VERSION_INFO\"/" switchcraft_modern.iss | ||
| sed -i "s/#define MyAppVersion \".*\"/#define MyAppVersion \"$DEV_VERSION\"/" switchcraft_legacy.iss | ||
| sed -i "s/#define MyAppVersionNumeric \".*\"/#define MyAppVersionNumeric \"$BASE_VERSION\"/" switchcraft_legacy.iss | ||
| sed -i "s/#define MyAppVersionInfo \".*\"/#define MyAppVersionInfo \"$VERSION_INFO\"/" switchcraft_legacy.iss |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same indentation concern applies here.
The sed patterns in this step have the same potential issue with indented #define lines as noted above. Apply the same fix with \(^[[:space:]]*\) capture group to preserve indentation.
🤖 Prompt for AI Agents
In @.github/workflows/release.yml around lines 188 - 200, The sed replacements
that set MyAppVersion, MyAppVersionNumeric, and MyAppVersionInfo in both
switchcraft_modern.iss and switchcraft_legacy.iss should preserve any leading
indentation; update each sed invocation that currently matches "#define
MyAppVersion", "#define MyAppVersionNumeric" and "#define MyAppVersionInfo" to
capture and reuse leading whitespace (e.g., a capture for ^[[:space:]]*) so the
replacement reinserts the same indentation while substituting DEV_VERSION,
BASE_VERSION and VERSION_INFO respectively for the six occurrences in the diff.
| def _ensure_winget_module(self) -> bool: | ||
| """ | ||
| Ensure Microsoft.WinGet.Client module is available. | ||
| Returns True if module is available or successfully installed, False otherwise. | ||
| """ | ||
| try: | ||
| ps_script = """ | ||
| if (-not (Get-Module -ListAvailable -Name Microsoft.WinGet.Client)) { | ||
| try { | ||
| Install-Module -Name Microsoft.WinGet.Client -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop | ||
| Write-Output "INSTALLED" | ||
| } catch { | ||
| Write-Output "FAILED: $_" | ||
| exit 1 | ||
| } | ||
| } else { | ||
| Write-Output "AVAILABLE" | ||
| } | ||
| """ | ||
| cmd = ["powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", ps_script] | ||
| startupinfo = self._get_startup_info() | ||
| kwargs = {} | ||
| if startupinfo: | ||
| kwargs['startupinfo'] = startupinfo | ||
| import sys | ||
| if sys.platform == "win32": | ||
| if hasattr(subprocess, 'CREATE_NO_WINDOW'): | ||
| kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW | ||
| else: | ||
| kwargs['creationflags'] = 0x08000000 | ||
| proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="ignore", timeout=60, **kwargs) | ||
| if proc.returncode == 0 and ("AVAILABLE" in proc.stdout or "INSTALLED" in proc.stdout): | ||
| return True | ||
| logger.warning(f"WinGet module check failed: {proc.stderr}") | ||
| return False | ||
| except Exception as e: | ||
| logger.debug(f"WinGet module check exception: {e}") | ||
| return False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Auto-installing PowerShell module without user consent.
The code silently installs Microsoft.WinGet.Client with -Force -AllowClobber if not present. This modifies the user's system without explicit consent, which may be unexpected behavior. Consider either:
- Logging an info message before attempting installation
- Making module installation opt-in via a configuration flag
- Simply returning False and relying on CLI fallback when the module is unavailable
💡 Suggested approach: Log and let user know
if (-not (Get-Module -ListAvailable -Name Microsoft.WinGet.Client)) {
try {
+ Write-Warning "Installing Microsoft.WinGet.Client module..."
Install-Module -Name Microsoft.WinGet.Client -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop
Write-Output "INSTALLED"Also consider adding a Python-side log:
if "INSTALLED" in proc.stdout:
logger.info("Automatically installed Microsoft.WinGet.Client PowerShell module")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _ensure_winget_module(self) -> bool: | |
| """ | |
| Ensure Microsoft.WinGet.Client module is available. | |
| Returns True if module is available or successfully installed, False otherwise. | |
| """ | |
| try: | |
| ps_script = """ | |
| if (-not (Get-Module -ListAvailable -Name Microsoft.WinGet.Client)) { | |
| try { | |
| Install-Module -Name Microsoft.WinGet.Client -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop | |
| Write-Output "INSTALLED" | |
| } catch { | |
| Write-Output "FAILED: $_" | |
| exit 1 | |
| } | |
| } else { | |
| Write-Output "AVAILABLE" | |
| } | |
| """ | |
| cmd = ["powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", ps_script] | |
| startupinfo = self._get_startup_info() | |
| kwargs = {} | |
| if startupinfo: | |
| kwargs['startupinfo'] = startupinfo | |
| import sys | |
| if sys.platform == "win32": | |
| if hasattr(subprocess, 'CREATE_NO_WINDOW'): | |
| kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW | |
| else: | |
| kwargs['creationflags'] = 0x08000000 | |
| proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="ignore", timeout=60, **kwargs) | |
| if proc.returncode == 0 and ("AVAILABLE" in proc.stdout or "INSTALLED" in proc.stdout): | |
| return True | |
| logger.warning(f"WinGet module check failed: {proc.stderr}") | |
| return False | |
| except Exception as e: | |
| logger.debug(f"WinGet module check exception: {e}") | |
| return False | |
| def _ensure_winget_module(self) -> bool: | |
| """ | |
| Ensure Microsoft.WinGet.Client module is available. | |
| Returns True if module is available or successfully installed, False otherwise. | |
| """ | |
| try: | |
| ps_script = """ | |
| if (-not (Get-Module -ListAvailable -Name Microsoft.WinGet.Client)) { | |
| try { | |
| Write-Warning "Installing Microsoft.WinGet.Client module..." | |
| Install-Module -Name Microsoft.WinGet.Client -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop | |
| Write-Output "INSTALLED" | |
| } catch { | |
| Write-Output "FAILED: $_" | |
| exit 1 | |
| } | |
| } else { | |
| Write-Output "AVAILABLE" | |
| } | |
| """ | |
| cmd = ["powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", ps_script] | |
| startupinfo = self._get_startup_info() | |
| kwargs = {} | |
| if startupinfo: | |
| kwargs['startupinfo'] = startupinfo | |
| import sys | |
| if sys.platform == "win32": | |
| if hasattr(subprocess, 'CREATE_NO_WINDOW'): | |
| kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW | |
| else: | |
| kwargs['creationflags'] = 0x08000000 | |
| proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="ignore", timeout=60, **kwargs) | |
| if proc.returncode == 0 and ("AVAILABLE" in proc.stdout or "INSTALLED" in proc.stdout): | |
| return True | |
| logger.warning(f"WinGet module check failed: {proc.stderr}") | |
| return False | |
| except Exception as e: | |
| logger.debug(f"WinGet module check exception: {e}") | |
| return False |
🤖 Prompt for AI Agents
In `@src/switchcraft_winget/utils/winget.py` around lines 536 - 573, In
_ensure_winget_module, stop silently forcing an install: add a configuration
flag (e.g. self.auto_install_winget or a param) and only attempt the
Install-Module block when that flag is True; if the flag is False, skip running
the ps_script install branch and immediately return False so the CLI fallback is
used. If you keep auto-installation enabled, log an info message before
attempting installation (logger.info(...)) and after a successful install
(detect "INSTALLED" in proc.stdout and logger.info("Automatically installed
Microsoft.WinGet.Client")). Update the function to reference the existing
ps_script, subprocess.run call and proc.stdout/proc.returncode checks to
implement the opt-in behavior and additional logs.
| ps_script = f""" | ||
| Import-Module Microsoft.WinGet.Client -ErrorAction SilentlyContinue | ||
| $pkg = Get-WinGetPackage -Id '{package_id}' -ErrorAction SilentlyContinue | ||
| if ($pkg) {{ | ||
| $pkg | Select-Object Name, Id, Version, Publisher, Description, Homepage, License, LicenseUrl, PrivacyUrl, Copyright, ReleaseNotes, Tags | ConvertTo-Json -Depth 2 | ||
| }} | ||
| """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Command injection vulnerability via unsanitized package_id.
Same issue as in _search_via_powershell: the package_id is interpolated directly into the PowerShell command without sanitization.
🔒 Proposed fix
+ # Sanitize package_id to prevent command injection
+ safe_id = package_id.replace("'", "''").replace("`", "``")
ps_script = f"""
Import-Module Microsoft.WinGet.Client -ErrorAction SilentlyContinue
- $pkg = Get-WinGetPackage -Id '{package_id}' -ErrorAction SilentlyContinue
+ $pkg = Get-WinGetPackage -Id '{safe_id}' -ErrorAction SilentlyContinue📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ps_script = f""" | |
| Import-Module Microsoft.WinGet.Client -ErrorAction SilentlyContinue | |
| $pkg = Get-WinGetPackage -Id '{package_id}' -ErrorAction SilentlyContinue | |
| if ($pkg) {{ | |
| $pkg | Select-Object Name, Id, Version, Publisher, Description, Homepage, License, LicenseUrl, PrivacyUrl, Copyright, ReleaseNotes, Tags | ConvertTo-Json -Depth 2 | |
| }} | |
| """ | |
| # Sanitize package_id to prevent command injection | |
| safe_id = package_id.replace("'", "''").replace("`", "``") | |
| ps_script = f""" | |
| Import-Module Microsoft.WinGet.Client -ErrorAction SilentlyContinue | |
| $pkg = Get-WinGetPackage -Id '{safe_id}' -ErrorAction SilentlyContinue | |
| if ($pkg) {{ | |
| $pkg | Select-Object Name, Id, Version, Publisher, Description, Homepage, License, LicenseUrl, PrivacyUrl, Copyright, ReleaseNotes, Tags | ConvertTo-Json -Depth 2 | |
| }} | |
| """ |
🤖 Prompt for AI Agents
In `@src/switchcraft_winget/utils/winget.py` around lines 582 - 588, The
PowerShell snippet in ps_script interpolates package_id directly (vulnerable to
command injection), so change the script to accept a parameter (e.g.,
param($id)) and reference $id inside the script instead of inserting package_id
into the string, and invoke the PowerShell process with an ArgumentList (or
equivalent API) to pass package_id as a separate argument; do the same pattern
used in _search_via_powershell (or adapt that secure approach) so package_id is
not concatenated into ps_script.
| ps_script = f""" | ||
| Import-Module Microsoft.WinGet.Client -ErrorAction SilentlyContinue | ||
| $result = Install-WinGetPackage -Id '{package_id}' -Scope {scope_param} -AcceptPackageAgreements -AcceptSourceAgreements -ErrorAction Stop | ||
| if ($result -and $result.ExitCode -eq 0) {{ | ||
| Write-Output "SUCCESS" | ||
| }} else {{ | ||
| Write-Output "FAILED" | ||
| exit 1 | ||
| }} | ||
| """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Command injection vulnerability in install path.
The package_id parameter is also unsanitized here. Apply the same sanitization fix.
🔒 Proposed fix
scope_param = "Machine" if scope == "machine" else "User"
+ safe_id = package_id.replace("'", "''").replace("`", "``")
ps_script = f"""
Import-Module Microsoft.WinGet.Client -ErrorAction SilentlyContinue
- $result = Install-WinGetPackage -Id '{package_id}' -Scope {scope_param} -AcceptPackageAgreements -AcceptSourceAgreements -ErrorAction Stop
+ $result = Install-WinGetPackage -Id '{safe_id}' -Scope {scope_param} -AcceptPackageAgreements -AcceptSourceAgreements -ErrorAction Stop🤖 Prompt for AI Agents
In `@src/switchcraft_winget/utils/winget.py` around lines 637 - 646, The PS script
builds a command using the unsanitized package_id (ps_script /
Install-WinGetPackage) leading to command injection; sanitize or validate
package_id the same way you handled other inputs (e.g., escape/whitelist or use
a safe helper function used elsewhere) before interpolating it into ps_script,
and ensure the sanitized value is used in the Install-WinGetPackage -Id
parameter and any related string construction.
| def _download_via_powershell(self, package_id: str, dest_dir: Path) -> Optional[Path]: | ||
| """Download a package using PowerShell (via Get-WinGetPackage and manual download).""" | ||
| try: | ||
| # PowerShell module doesn't have a direct download cmdlet, so we fall back to CLI | ||
| # But we can use it to verify the package exists first | ||
| if not self._ensure_winget_module(): | ||
| return None | ||
|
|
||
| ps_script = f""" | ||
| Import-Module Microsoft.WinGet.Client -ErrorAction SilentlyContinue | ||
| $pkg = Get-WinGetPackage -Id '{package_id}' -ErrorAction SilentlyContinue | ||
| if ($pkg) {{ | ||
| Write-Output "EXISTS" | ||
| }} else {{ | ||
| Write-Output "NOT_FOUND" | ||
| exit 1 | ||
| }} | ||
| """ | ||
| cmd = ["powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", ps_script] | ||
| startupinfo = self._get_startup_info() | ||
| kwargs = {} | ||
| if startupinfo: | ||
| kwargs['startupinfo'] = startupinfo | ||
| import sys | ||
| if sys.platform == "win32": | ||
| if hasattr(subprocess, 'CREATE_NO_WINDOW'): | ||
| kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW | ||
| else: | ||
| kwargs['creationflags'] = 0x08000000 | ||
| proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="ignore", timeout=30, **kwargs) | ||
|
|
||
| # If package exists, use CLI to download (PowerShell module doesn't have download cmdlet) | ||
| if proc.returncode == 0 and "EXISTS" in proc.stdout: | ||
| # Fall through to CLI download | ||
| return None | ||
| return None | ||
| except Exception as e: | ||
| logger.debug(f"PowerShell download check error: {e}") | ||
| return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Function name is misleading; always returns None.
_download_via_powershell doesn't actually download anything—it only verifies the package exists, then returns None in all cases (lines 696-699). The CLI fallback in download_package handles the actual download regardless.
Consider either:
- Renaming to
_verify_package_exists_via_powershell - Removing this function entirely since the existence check adds a round-trip without meaningful benefit
Also, line 674 has the same command injection vulnerability with package_id.
💡 Suggested simplification
If the verification doesn't prevent CLI download attempts, consider removing this pre-check:
def download_package(self, package_id: str, dest_dir: Path) -> Optional[Path]:
"""Download a package installer to dest_dir using Winget CLI."""
- # Try PowerShell first (preferred method)
- try:
- result = self._download_via_powershell(package_id, dest_dir)
- if result:
- return result
- except Exception as e:
- logger.debug(f"PowerShell download failed for {package_id}, falling back to CLI: {e}")
-
- # Fallback to CLI
cmd = ["winget", "download", "--id", package_id, ...🤖 Prompt for AI Agents
In `@src/switchcraft_winget/utils/winget.py` around lines 664 - 702, The
_download_via_powershell function is misleading because it never downloads and
always returns None; either remove the function or rename it (e.g.,
_verify_package_exists_via_powershell) and adjust callers (like
download_package) accordingly so behavior is clear; if you keep the existence
check, ensure it actually affects control flow (return a boolean/raise on not
found) and avoid command injection by not interpolating package_id into the
PowerShell string—pass it as a safe argument or validate/escape it before use,
and update any logic that expects a Path (e.g., callers of
_download_via_powershell) to handle the new return type.
| logger.info(f"Fetching package details for: {short_info['Id']}") | ||
| full = self.winget.get_package_details(short_info['Id']) | ||
| logger.debug(f"Raw package details received: {list(full.keys()) if full else 'empty'}") | ||
| merged = {**short_info, **full} | ||
| self.current_pkg = merged | ||
| logger.info(f"Package details fetched, showing UI for: {merged.get('Name', 'Unknown')}") | ||
| logger.info(f"Merged package data keys: {list(merged.keys())}") | ||
|
|
||
| # Validate that we got some data | ||
| if not full: | ||
| logger.warning(f"get_package_details returned empty dict for {short_info['Id']}") | ||
| raise Exception(f"No details found for package: {short_info['Id']}") | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate details before merging to avoid a TypeError
full can be None; {**short_info, **full} will raise before your intended “No details found” error. Validate full (or coerce to {}) before the merge.
🐛 Proposed fix
- full = self.winget.get_package_details(short_info['Id'])
- logger.debug(f"Raw package details received: {list(full.keys()) if full else 'empty'}")
- merged = {**short_info, **full}
- self.current_pkg = merged
+ full = self.winget.get_package_details(short_info['Id'])
+ logger.debug(f"Raw package details received: {list(full.keys()) if full else 'empty'}")
+ if not full:
+ logger.warning(f"get_package_details returned empty dict for {short_info['Id']}")
+ raise Exception(f"No details found for package: {short_info['Id']}")
+ merged = {**short_info, **full}
+ self.current_pkg = merged🤖 Prompt for AI Agents
In `@src/switchcraft/gui_modern/views/winget_view.py` around lines 372 - 384, The
code merges short_info and full without checking full, which can be None and
cause a TypeError; update the block after calling
self.winget.get_package_details(short_info['Id']) (the get_package_details call)
to validate/coerce full (e.g., if full is None: log a warning and set full = {}
or raise immediately) before running merged = {**short_info, **full}, then set
self.current_pkg = merged and keep the existing logger messages; ensure the
existing check that raises on empty details still runs after the validation or
replace it with the immediate None-handling logic.
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.