Skip to content

Windows: scope OpenClaw detection to the scanned user - #268

Open
anonpran wants to merge 3 commits into
stagingfrom
nanda/win-openclaw-scoping
Open

anonpran wants to merge 3 commits into
stagingfrom
nanda/win-openclaw-scoping

Conversation

@anonpran

@anonpran anonpran commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Completes the per-user scoping work from #265 and #266. OpenClaw is not in detect_tool_for_user's dispatch list, so it falls through to the default detector.detect() and both PRs missed it.

Problem

Every one of OpenClaw's probes resolves the scanner's profile, never the assigned self.user_home:

Source Reads
_check_binary():104 shutil.which("openclaw") — the scanner's PATH, not even admin-gated
_get_installation_paths():126,137 %LOCALAPPDATA% (scanner's) + the admin C:\Users sweep
_search_for_executable():170 %LOCALAPPDATA% / %APPDATA% (scanner's)

Reproduced by driving the real detect_tool_for_user() against a two-profile tree where only alice has OpenClaw:

privilege     scanner  alice   bob     verdict
non-elevated  alice    True    True    WRONG   <- bob credited with alice's install
non-elevated  bob      False   False   WRONG   <- alice's real install missed
admin         alice    True    True    WRONG
admin         bob      True    True    WRONG

BEFORE: fan-out / missed install in 4/4 scenarios
AFTER:  no fan-out (4/4 correct)

Same bidirectional FP/FN signature as #265 and #266.

Scoping only the :137 sweep is not sufficient. Verified separately — with the install dirs scoped away but openclaw on the scanner's PATH, _check_binary alone still credits every user:

install dirs scoped away, openclaw on the SCANNER's PATH:
   alice=True  bob=True  -> still fans out via _check_binary()

Fix

A scoped branch on self.user_home covering all three sources at once: probe that user's AppData\Local\Programs\OpenClaw plus the machine-wide Program Files locations, which are legitimately every user's. Unscoped/legacy behaviour is untouched.

Two deliberate choices:

  • _check_running_process is not reused in the scoped path. A running process cannot be attributed to a specific profile without an owner lookup, and it yields no install_path — the backend drops a tool report without one (ai_tools_service.py:6269-6270), so nothing storable is lost.
  • except PermissionError, not except (PermissionError, OSError), following the Greptile P1 finding on [WEB-5697] Windows: scope Copilot to the scanned user, and stop profile probes crashing on access-denied #266: access-denied should skip the candidate, but a transient I/O failure must propagate so the run is marked incomplete rather than read as "not installed" and pruned.

Tests

TestWindowsOpenClawPerUserScoping in tests/test_scan_completed_manifest.py:

  1. Scoped to the scanned user — across both privilege levels and both scanner identities, with shutil.which and _check_running_process deliberately returning hits so the test fails unless all three sources are scoped.
  2. Machine-wide install survives scoping — guards against over-correcting.
  3. Unscoped legacy path unchanged.

All three fail against staging. Regression green: probe-permission safety, Copilot scoping, IDE scoping, manifest-key, manifest-from-presence. Raisers on an access-denied home stay at 0/19.

Note

This is the last detector in the registered Windows set that ignored its assigned user_home. Verified by driving every registered detector with Path.home() pinned to a foreign profile — 0 leak onto the scanned user, before and after.

🤖 Generated with Claude Code

Greptile Summary

The PR scopes Windows OpenClaw detection to the assigned user while preserving machine-wide installations and legacy unscoped behavior.

  • Walks the scanned user’s Local and Roaming AppData instead of the scanner account’s environment-derived paths.
  • Recognizes executable, CMD, PowerShell, and extensionless OpenClaw command files.
  • Adds regression coverage for cross-user attribution, Squirrel-style installs, npm shims, machine-wide installs, and legacy behavior.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
scripts/coding_discovery_tools/windows/openclaw/detect_openclaw.py Adds user-scoped OpenClaw discovery and completes the previously reported deep-search and npm-shim fixes without leaving an eligible blocking defect.
tests/test_scan_completed_manifest.py Adds focused regression coverage for user attribution, supported executable layouts, machine-wide installations, and unscoped compatibility.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Windows OpenClaw detection] --> B{user_home assigned?}
    B -->|No| C[Legacy PATH, environment, and process probes]
    B -->|Yes| D[Check scanned user's known install directory]
    D --> E[Check machine-wide OpenClaw directories]
    E --> F[Walk scanned user's Local and Roaming AppData]
    F --> G[Match exe, cmd, ps1, or extensionless command]
    C --> H[Normalized OpenClaw result]
    G --> H
Loading

Reviews (3): Last reviewed commit: "Address Greptile: match npm-global shims..." | Re-trigger Greptile

OpenClaw is not in detect_tool_for_user's dispatch list, so it falls through to
the default detector.detect() and was missed by #265 and #266. All three of its
probes resolve the SCANNER's profile rather than the assigned user_home:
_check_binary (:104, shutil.which on PATH, not even admin-gated),
_get_installation_paths (:126 %LOCALAPPDATA% + :137 admin C:\Users sweep), and
_search_for_executable (:170 %LOCALAPPDATA%/%APPDATA%). One user's install is
credited to every profile; when the scanner lacks it, a real install is missed.

Reproduced 4/4 scenarios, same bidirectional FP/FN as #265/#266. Scoping only
the :137 sweep is not enough -- verified that _check_binary alone still fans out.

Adds a scoped branch covering all three. _check_running_process is skipped in
the scoped path: it yields no install_path, and the backend drops a tool report
without one (ai_tools_service.py:6269). Machine-wide Program Files stays for
everyone; unscoped/legacy behaviour is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oDYoAEoWFn89jksQhpq3m
@anonpran
anonpran requested a review from a team September 2, 2026 11:42
Comment thread scripts/coding_discovery_tools/windows/openclaw/detect_openclaw.py
The scoped branch checked three exact directories, but the unscoped path also
runs an os.walk of %LOCALAPPDATA%/%APPDATA% for openclaw.exe. Squirrel installs
directly under %LOCALAPPDATA%\OpenClaw (not Programs) -- the same layout the
Replit detector documents -- so a genuine install outside Programs was found
before this PR and dropped after it, then treated as uninstalled.

_search_for_executable now takes optional roots, so the scoped path runs the
same walk rooted at the SCANNED user's AppData instead of the env-derived
(scanner's) one. Coverage preserved, attribution still fixed. Exact-dir-first
then deep-walk order matches the unscoped path, so the cost profile is unchanged.

Only _check_binary (PATH) and _check_running_process stay dropped in the scoped
path: neither can be attributed to a profile, and the latter yields no
install_path, which the backend discards anyway.

Adds a test pinning an install at %LOCALAPPDATA%\OpenClaw\app-1.2.3\openclaw.exe
-- found for its owner, not leaked to the co-resident user.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oDYoAEoWFn89jksQhpq3m

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

1 finding — 0 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

Medium · 🟡 TRIAGE — subprocess.run(..., shell=True) in legacy detection path

scripts/coding_discovery_tools/windows/openclaw/detect_openclaw.py:123

Impact: Shell invocation inherits scanner environment and expands metacharacters; risky if any argument is derived from filesystem or user-controlled data.

Fix: Pre-existing code (unchanged by this PR). Prefer shell=False with a fixed argv list, or validate/sanitize inputs before any shell execution.

Flagged by: Semgrep


Diff review (Cursor): The new _detect_for_user path only probes fixed candidate directories with path.exists(), catches PermissionError only (per intentional design — transient I/O errors propagate), and does not add subprocess, network, or secret-handling. No new injection, privilege-escalation, or cross-user attribution vectors in the introduced logic. Claude and Gitleaks reported clean.


🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head ed39f4b5 · 2026-09-02T11:51Z

Comment thread scripts/coding_discovery_tools/windows/openclaw/detect_openclaw.py

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

1 finding — 0 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

Findings

🟡 TRIAGE — subprocess.run with shell=True

scripts/coding_discovery_tools/windows/openclaw/detect_openclaw.py:123

Impact: Spawning a shell inherits the scanner process environment and expands injection risk if any argument is ever derived from untrusted input.

Fix: Prefer shell=False with an argv list; if a shell is required, use a fixed, fully quoted command with no interpolated paths or env values.

Flagged by: Semgrep


Previously acknowledged (not re-flagged)

  • Scoped executable search too narrow (missed Squirrel / deep AppData layouts) — Fixed in 7bc4768; scoped path now runs the same _search_for_executable walk rooted at the scanned user's AppData via search_roots (@anonpran).
  • _check_binary / _check_running_process dropped in scoped path — Accepted by design: PATH resolves the scanner profile (fan-out source); running processes cannot be profile-attributed and yield no install_path (PR description / @anonpran).
  • except PermissionError only, not OSError — Accepted design choice so transient I/O errors fail the scan instead of being read as "not installed" (#266 / PR description).

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 7bc47689 · 2026-09-02T12:03Z

Measured before/after, and half of this was a regression this PR introduced:

                        shim ON scanner PATH   shim NOT on PATH
  staging (pre-PR)      DETECTED               missed
  this PR (before fix)  missed                 missed
  this PR (after fix)   DETECTED               DETECTED

The scoped path drops _check_binary (it resolves the SCANNER's PATH -- the
fan-out source), and shutil.which on Windows resolves .cmd via PATHEXT, so
dropping it lost the one probe that saw an npm shim for the scanning user's own
profile. The deep walk did not cover it: it matched "openclaw.exe" only, which
is also why the not-on-PATH case was already missed on staging.

The walk now matches _EXECUTABLE_NAMES (.exe, .cmd, .ps1, extensionless),
ordered so a real .exe still wins over a shim in the same directory. Applies to
both the scoped and unscoped paths -- one function, and divergence there is what
caused this class of bug. Net effect vs staging is strictly more coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oDYoAEoWFn89jksQhpq3m

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)


🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head ca77f93d · 2026-09-02T12:13Z

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.

3 participants