Skip to content

P0: Update/Upgrade guardrails for system scope (admin/elevation required)#33

Merged
Kinin-Code-Offical merged 1 commit intomainfrom
p0/upgrade-guardrails
Dec 21, 2025
Merged

P0: Update/Upgrade guardrails for system scope (admin/elevation required)#33
Kinin-Code-Offical merged 1 commit intomainfrom
p0/upgrade-guardrails

Conversation

@Kinin-Code-Offical
Copy link
Owner

@Kinin-Code-Offical Kinin-Code-Offical commented Dec 21, 2025

Closes #16

Summary:

  • Require elevation for system-scope installer updates when not already admin.
  • Block portable updates to system-scope installs unless running as admin.
  • Avoid unnecessary RunAs when already elevated.

Tests:

  • npm ci (PASS)
  • npm run lint (PASS)
  • npm test (PASS)
  • npm run build (PASS)
  • npm run package (PASS)
  • npm run smoke:exe (PASS)
  • npm run installer (PASS)
  • npm run stage (PASS)

Rollback:

  • Revert this commit to allow non-elevated system-scope upgrade attempts.

Summary by Sourcery

Enforce elevation requirements when upgrading system-scope installs and prevent unsafe portable updates to system-wide installations.

New Features:

  • Add admin detection to the upgrade command to distinguish between elevated and non-elevated runs.

Bug Fixes:

  • Prevent non-elevated system-scope installer upgrades by requiring elevation unless explicitly disabled.
  • Block portable updates for system-scope installations when not running as admin to avoid permission issues.
  • Avoid unnecessary elevation prompts when the process is already running with admin rights.

Copilot AI review requested due to automatic review settings December 21, 2025 23:49
@sourcery-ai
Copy link

sourcery-ai bot commented Dec 21, 2025

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Enforces stricter elevation rules for system-scope upgrades by detecting admin status and install location, blocking unsafe paths, and only elevating when necessary during installer-based updates.

Sequence diagram for upgraded elevation logic in upgrade command

sequenceDiagram
    actor User
    participant UpgradeCommand
    participant InstallContext as detectInstallContext
    participant AdminCheck as isAdmin
    participant InstallerUpdate as applyUpdateInstaller
    participant PortableUpdate as applyUpdatePortableExe

    User->>UpgradeCommand: invoke upgrade(options)
    alt asset option is auto
        UpgradeCommand->>InstallContext: detectInstallContext()
        InstallContext-->>UpgradeCommand: context
    else asset option is explicit
        UpgradeCommand-->>UpgradeCommand: context = options.asset
    end

    UpgradeCommand->>AdminCheck: isAdmin()
    AdminCheck-->>UpgradeCommand: admin

    UpgradeCommand-->>UpgradeCommand: systemScope = isSystemScopePath(process.execPath)

    alt context is installer and not admin and options.elevate is false
        UpgradeCommand-->>User: throw error System-scope update requires elevation
    else context is installer or setup exe
        UpgradeCommand-->>UpgradeCommand: shouldElevate = not admin and options.elevate not false
        UpgradeCommand->>InstallerUpdate: applyUpdateInstaller(downloadPath, silent, shouldElevate)
        InstallerUpdate-->>UpgradeCommand: update applied
    else portable update path
        alt systemScope and not admin
            UpgradeCommand-->>User: throw error Portable updates require admin
        else not systemScope or admin
            UpgradeCommand->>PortableUpdate: applyUpdatePortableExe(downloadPath, targetExe)
            PortableUpdate-->>UpgradeCommand: update applied
        end
    end
Loading

Flow diagram for system-scope upgrade elevation guardrails

flowchart TD
    A["Start upgrade command"] --> B["Determine context (installer or portable)"]
    B --> C["Call isAdmin"]
    C --> D["Compute systemScope from process.execPath"]

    D --> E{context is installer or setup exe?}

    E -->|Yes| F{admin is false and options.elevate is false?}
    F -->|Yes| G["Error: System-scope update requires elevation"]
    F -->|No| H["shouldElevate = not admin and options.elevate not false"]
    H --> I["applyUpdateInstaller(downloadPath, silent, shouldElevate)"]
    I --> Z["End"]

    E -->|No - portable| J{systemScope is true and admin is false?}
    J -->|Yes| K["Error: Portable update to system-scope requires admin"]
    J -->|No| L["applyUpdatePortableExe(downloadPath, targetExe)"]
    K --> Z
    L --> Z
Loading

File-Level Changes

Change Details Files
Add system-scope detection and admin check to gate upgrade behavior.
  • Import isAdmin helper from PowerShell integration module.
  • Introduce isSystemScopePath helper to classify system-scope installations based on common Program Files and ProgramData paths.
  • Determine admin status and whether current executable path is system-scope at upgrade runtime.
src/commands/upgrade.ts
Enforce elevation requirement for system-scope installer upgrades and avoid redundant elevation.
  • Throw an explicit error when attempting an installer-based system-scope update without elevation and with --no-elevate set.
  • Compute shouldElevate based on current admin status and the --elevate flag instead of always requesting elevation.
  • Pass shouldElevate into applyUpdateInstaller to skip RunAs when already admin.
src/commands/upgrade.ts
Block portable updates that target system-scope installs when not running as admin.
  • On portable update path, detect when process executable resides in a system-scope directory and admin is false.
  • Throw a clear error instructing users to use the installer or rerun as admin for system-scope portable upgrades.
src/commands/upgrade.ts

Assessment against linked issues

Issue Objective Addressed Explanation
#16 Require elevation for system-scope installer updates when not already running as admin.
#16 Block portable updates to system-scope installs when not running as admin.
#16 Avoid unnecessary elevation (RunAs) when the process is already running as admin.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The isSystemScopePath helper currently relies on hard-coded substrings like \program files and assumes Windows-style paths; consider guarding this logic with a process.platform === 'win32' check and/or using process.env.ProgramFiles, ProgramFiles(x86), and ProgramData plus path.resolve/startsWith to avoid false positives and platform issues.
  • Right now systemScope is derived from process.execPath only; if there are cases where the target of the update differs from the current executable, it may be safer to base the system-scope check on the actual install/update target path rather than the current process path.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `isSystemScopePath` helper currently relies on hard-coded substrings like `\program files` and assumes Windows-style paths; consider guarding this logic with a `process.platform === 'win32'` check and/or using `process.env.ProgramFiles`, `ProgramFiles(x86)`, and `ProgramData` plus `path.resolve`/`startsWith` to avoid false positives and platform issues.
- Right now `systemScope` is derived from `process.execPath` only; if there are cases where the target of the update differs from the current executable, it may be safer to base the system-scope check on the actual install/update target path rather than the current process path.

## Individual Comments

### Comment 1
<location> `src/commands/upgrade.ts:99-100` </location>
<code_context>
+            const admin = await isAdmin();
+            const systemScope = isSystemScopePath(process.execPath);
+
+            if (context === 'installer' && !admin && options.elevate === false) {
+                throw new Error('System-scope update requires elevation. Re-run without --no-elevate or run as admin.');
+            }

</code_context>

<issue_to_address>
**issue (bug_risk):** Installer-elevation requirement is only enforced when `context === 'installer'`, not when an installer is inferred from the asset name.

The installer path is actually selected with `context === 'installer' || (asset.name.endsWith('.exe') && asset.name.includes('setup'))`, but this pre-check only covers `context === 'installer'`. If the filename implies an installer while `context !== 'installer'`, a non-admin using `--no-elevate` will still reach `applyUpdateInstaller` with `shouldElevate = false` and won’t see this explicit error. Consider basing this check on the same predicate as the installer branch (or re-checking inside that branch) so elevation behavior and messaging are consistent regardless of how installer context is derived.
</issue_to_address>

### Comment 2
<location> `src/commands/upgrade.ts:109-110` </location>
<code_context>
+                await applyUpdateInstaller(downloadPath, options.silent !== false, shouldElevate);
             } else {
                 if (!options.json) logger.info('Applying portable update...');
+                if (systemScope && !admin) {
+                    throw new Error('Portable updates to system-scope installs require admin. Use the installer or re-run as admin.');
+                }
                 // For portable, we need to know the target exe. 
</code_context>

<issue_to_address>
**issue (bug_risk):** The portable system-scope check may block dev/node environments where `process.execPath` is under Program Files, changing previous behavior.

On Windows dev setups, `process.execPath` for Node is typically under `C:\Program Files\nodejs\node.exe`, so `systemScope && !admin` will be true and this branch will now throw before the later packaged-vs-plain-Node logic (which previously only warned about not updating `node.exe`). If you still want warn-only behavior in dev, consider adjusting this condition—e.g., skip the system-scope check when running under `node.exe`, or move it after the packaged-vs-node determination so dev scenarios continue to work.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +99 to +100
if (context === 'installer' && !admin && options.elevate === false) {
throw new Error('System-scope update requires elevation. Re-run without --no-elevate or run as admin.');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Installer-elevation requirement is only enforced when context === 'installer', not when an installer is inferred from the asset name.

The installer path is actually selected with context === 'installer' || (asset.name.endsWith('.exe') && asset.name.includes('setup')), but this pre-check only covers context === 'installer'. If the filename implies an installer while context !== 'installer', a non-admin using --no-elevate will still reach applyUpdateInstaller with shouldElevate = false and won’t see this explicit error. Consider basing this check on the same predicate as the installer branch (or re-checking inside that branch) so elevation behavior and messaging are consistent regardless of how installer context is derived.

Comment on lines +109 to +110
if (systemScope && !admin) {
throw new Error('Portable updates to system-scope installs require admin. Use the installer or re-run as admin.');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The portable system-scope check may block dev/node environments where process.execPath is under Program Files, changing previous behavior.

On Windows dev setups, process.execPath for Node is typically under C:\Program Files\nodejs\node.exe, so systemScope && !admin will be true and this branch will now throw before the later packaged-vs-plain-Node logic (which previously only warned about not updating node.exe). If you still want warn-only behavior in dev, consider adjusting this condition—e.g., skip the system-scope check when running under node.exe, or move it after the packaged-vs-node determination so dev scenarios continue to work.

@Kinin-Code-Offical Kinin-Code-Offical merged commit 29593e0 into main Dec 21, 2025
9 checks passed
@Kinin-Code-Offical Kinin-Code-Offical deleted the p0/upgrade-guardrails branch December 21, 2025 23:51
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances security guardrails for system-scope installations by requiring elevation for installer updates and blocking portable updates to system directories unless running as admin. The changes prevent unauthorized modifications to system-scope installations while avoiding unnecessary elevation prompts when already elevated.

Key Changes:

  • Added elevation requirement check for installer updates when --no-elevate flag is used
  • Added system-scope path detection to block portable updates to protected directories
  • Modified elevation logic to avoid redundant RunAs when already admin

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +109 to +111
if (systemScope && !admin) {
throw new Error('Portable updates to system-scope installs require admin. Use the installer or re-run as admin.');
}
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The portable update guard for system-scope installs is security-critical but lacks test coverage. This condition prevents non-admin users from attempting portable updates to system directories. Consider adding tests to verify this protection works correctly for different combinations of systemScope paths and admin status.

Copilot uses AI. Check for mistakes.
const systemScope = isSystemScopePath(process.execPath);

if (context === 'installer' && !admin && options.elevate === false) {
throw new Error('System-scope update requires elevation. Re-run without --no-elevate or run as admin.');
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message states "System-scope update requires elevation" but the condition only checks if context is 'installer' and not admin, without verifying that it's actually a system-scope installation. This could be misleading when an installer update is attempted from a user-scope installation. Consider checking systemScope variable in this condition as well, or update the error message to say "Installer update requires elevation" to be more accurate.

Suggested change
throw new Error('System-scope update requires elevation. Re-run without --no-elevate or run as admin.');
throw new Error('Installer update requires elevation. Re-run without --no-elevate or run as admin.');

Copilot uses AI. Check for mistakes.
throw new Error('System-scope update requires elevation. Re-run without --no-elevate or run as admin.');
}

if (context === 'installer' || asset.name.endsWith('.exe') && asset.name.includes('setup')) {
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition could be clearer with explicit parentheses to show the intended operator precedence. While JavaScript evaluates this as (context === 'installer') || (asset.name.endsWith('.exe') && asset.name.includes('setup')), adding parentheses would make the logic more explicit and easier to understand for future maintainers.

Suggested change
if (context === 'installer' || asset.name.endsWith('.exe') && asset.name.includes('setup')) {
if (context === 'installer' || (asset.name.endsWith('.exe') && asset.name.includes('setup'))) {

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +19
function isSystemScopePath(filePath: string): boolean {
const normalized = filePath.toLowerCase();
return normalized.includes('\\program files') || normalized.includes('\\program files (x86)') || normalized.includes('\\programdata');
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path checking logic uses Windows-specific path separators (backslashes) and is case-sensitive to lowercase normalization. This function assumes Windows paths but doesn't document this assumption. Consider adding a comment or JSDoc to clarify this function is Windows-specific, or handle the case where forward slashes might be present in normalized Windows paths.

Suggested change
function isSystemScopePath(filePath: string): boolean {
const normalized = filePath.toLowerCase();
return normalized.includes('\\program files') || normalized.includes('\\program files (x86)') || normalized.includes('\\programdata');
/**
* Checks whether the given path points to a common Windows system-wide
* installation directory. This is intentionally Windows-specific and
* normalizes the path to Windows-style separators before comparison.
*/
function isSystemScopePath(filePath: string): boolean {
const normalized = path
.normalize(filePath)
.toLowerCase()
.replace(/\//g, '\\');
return (
normalized.includes('\\program files') ||
normalized.includes('\\program files (x86)') ||
normalized.includes('\\programdata')
);

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +101
if (context === 'installer' && !admin && options.elevate === false) {
throw new Error('System-scope update requires elevation. Re-run without --no-elevate or run as admin.');
}
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new elevation guard logic is security-critical but lacks test coverage. The condition checks if context is 'installer', user is not admin, and elevation is explicitly disabled. Consider adding tests to verify this guard works correctly for different combinations of admin status, install context, and elevation options to prevent security bypass scenarios.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P0: Update/Upgrade guardrails for system scope (admin/elevation required)

1 participant