Conversation
| return users | ||
|
|
||
|
|
||
| WINDOWS_USER_SID_RE = re.compile(r"^S-1-5-21-\d+-\d+-\d+-(\d+)$") |
There was a problem hiding this comment.
Entra profiles fail SID classification
When a device has an Entra ID profile with an S-1-12-1 SID alongside a qualifying S-1-5-21 profile, this regex excludes the Entra profile from the nonempty allowlist, causing that user's tools, plugins, and rules to be omitted from discovery.
Knowledge Base Used: Platform filesystem extraction
| and not user_dir.name.startswith('.') | ||
| and user_dir.name not in WINDOWS_SKIP_USER_DIRS): | ||
| and user_dir.name not in WINDOWS_SKIP_USER_DIRS | ||
| and (not human_profiles or user_dir.name.lower() in human_profiles)): |
There was a problem hiding this comment.
Empty results disable profile filtering
When ProfileList is readable but contains no qualifying entries, or every qualifying entry raises an entry-level error, the helper returns the same empty set as an unreadable registry. This condition then admits every non-blacklisted directory under C:\Users, causing service profiles and localized junctions to be scanned and attributed as users.
Knowledge Base Used:
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
1 finding — 0 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
🟡 TRIAGE — Empty ProfileList allowlist disables SID filtering (fail-open regression)
scripts/coding_discovery_tools/utils.py:451
Impact: When ProfileList is readable but yields an empty allowlist (no qualifying S-1-5-21 entries, or every entry errors out), _get_windows_human_profile_dirs() returns frozenset() and get_all_users_windows() treats that the same as an unreadable registry (not human_profiles), re-admitting all non-blacklisted C:\Users directories—including IIS app pools, service profiles, and localized junctions—and restoring cross-user phantom attribution / over-collection.
Fix: Return a sentinel or tuple from _get_windows_human_profile_dirs() to distinguish “registry unreadable → fail open” from “registry read OK, zero matches → fail closed (or blacklist-only)”; only skip SID filtering on the former.
Flagged by: Cursor (lead), Greptile (inline on :451; not in automated reviewer set but noted in PR discussion)
Previously acknowledged (not re-flagged)
None — no maintainer replies marking items as accepted-by-design, false positive, or wontfix in the provided thread.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 3359ecb9 · 2026-08-31T13:02Z
Both detectors built their candidate list from Path.home() instead of the user_home that detect_tool_for_user assigns. Under a logged-in scan that credited one user's install to every other profile on the box; under SYSTEM it resolved to systemprofile and found nothing. Falls back to Path.home() when user_home is unset, matching windows/copilot_cli. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RYVuhiiJ8mW3T1P1soDFLR
3359ecb to
10405bb
Compare
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 10405bbe · 2026-08-31T13:31Z
The old assertions rooted the fake home in tempfile and asserted the candidate paths did not start with Path.home(). On Windows %TEMP% lives under the home directory, so that failed on any real Windows box; CI passed only because GitHub exposes TEMP as an 8.3 short name. Assert positively against a synthetic home instead, and add a detect_tool_for_user cross-user case covering the defect as it appeared in production. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RYVuhiiJ8mW3T1P1soDFLR
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)
Previously acknowledged (not re-flagged)
- Weak Windows profile enumeration (
WINDOWS_SKIP_USER_DIRSonly) — Maintainer deferred to a separate ticket; out of scope for this PR, which fixes per-user path scoping only. - Empty/unreadable ProfileList admits all
C:\Usersdirs — Same enumeration ticket; acknowledged as a distinct defect, not introduced here. - Entra ID (
S-1-12-1) SID profiles excluded from allowlist — Related enumeration/classification issue; deferred alongside the ProfileList work.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 78a80a82 · 2026-08-31T14:30Z
The bug
detect_tool_for_userloops over every profile on the machine and tells each detector which user it is scanning:The Windows Cursor and Windsurf detectors discard that and build their candidate list from the scanner's own home:
So every profile on the box is checked against one folder.
Effect
Path.home()resolves toC:\Users\KapilNT AUTHORITY\SYSTEMC:\Windows\system32\config\systemprofileMeasured in prod: 28 of 42 Windows Cursor/Windsurf inventory rows are cross-user phantoms (20 Cursor, 8 Windsurf, 67%). All
install_pathvalues point at a real user's directory and none atsystemprofile, so these came from the logged-in variant.Both IIS app-pool identities and unrelated real colleagues are credited with someone else's install.
Fix
Use the home the loop already resolved, falling back to current behaviour when it is unset:
getattrrather thanself.user_homeis load-bearing, not defensive padding:BaseToolDetector(coding_tool_base.py:36) has no__init__and never declaresuser_home, the factory constructs these detectors bare (coding_tool_factory.py:110,194), and three live callers reachdetect()without setting it (ai_tools_discovery.py:470,:498,scripts/coding_discovery_tools/test.py:35).self.user_homewould raiseAttributeErroron those paths.It is also the dominant idiom in this repo:
windows/cline/cline.py:70,windows/roo_code/roo_code.py:70,windows/kilocode/kilocode.py:71and their macOS/Linux twins use the samegetattrform, andwindows/claude_cowork/claude_cowork.py:109uses the identical or-chain.Machine-global entries (
C:\Program Files\Cursor) are untouched and still probed, by design.What it brings
Rollout caveat:
install.ps1never invokessetup-scheduled-scan.ps1, so the existing Windows fleet does not re-scan on a schedule. Until that is fixed these rows will not actually be re-reported and pruned, so treat the inventory correction as pending fleet re-scan rather than automatic.One narrow, accepted loss: for a scanning user whose profile is not under
C:\Users(redirected profile), the pre-fix code found their Cursor viaPath.home()and misattributed it to some other profile. Post-fix that row disappears rather than moving. Trading a wrong-owner row for a missing row is the right direction.Verification
tests/test_windows_cursor_windsurf_user_scope.py, 8 tests across both detectors: candidate paths followuser_home, fall back toPath.home()when unset, machine-global paths stay unscoped, and adetect_tool_for_usercross-user case asserting user A's install is not reported for user B (the defect exactly as it appeared in prod).Homes are synthetic rather than
tempfile-rooted: on Windows%TEMP%lives underPath.home(), so atempfileroot plus aPath.home()negative assertion fails on any real Windows box and passes CI only because GitHub exposesTEMPas an 8.3 short name. Verified by re-running withTMPDIRforced under$HOME.Mutation-checked: reverting the two production lines fails 4 of the 8. 66 tests pass across this file plus the Cursor/Windsurf residue suites,
test_windows_cline_roo_antigravity_scopeandtest_user_filtering. All six CI legs green, includingwindows-lateston 3.9/3.11/3.12.The affected rows are identifiable in advance, so the rollout can be asserted rather than eyeballed: snapshot the
(device_id, tool_name, home_user)set where theinstall_pathuser segment differs fromhome_user, then confirm the post-rollout delta matches exactly.On the enumeration finding
A bot review flagged that
get_all_users_windows()(utils.py:380-405) still filters only against the five names inWINDOWS_SKIP_USER_DIRS(constants.py:146-148), so service profiles and localized junctions are still scanned. That is accurate and is a real defect, but it is separate and does not block this change.Scoped precisely: for per-user (AppData/Squirrel) installs, a phantom row needs both weak enumeration and this
Path.home()bug, so this PR alone removes that class — which is 28 of the 28 observed rows. For a machine-wide install,C:\Program Files\Cursorstays unscoped by design, sodetect()still returns a hit for a junk profile and enumeration alone is sufficient to produce a phantom. That residual class is genuinely the enumeration ticket's, and none of the observed rows fall into it.The enumeration fix was deliberately dropped from this PR: the natural implementation matches
ProfileListbyProfileImagePathbasename, which can reject real users when profiles are container-backed (FSLogix, Cloud PC), and that is the current leading hypothesis for the unexplained Cloud PC/RDS blindness. Filed separately rather than gambling there for telemetry hygiene.Context
Found while investigating
DISCOVERY-TOOL-SCRIPT-17("Discovery found no tools", 3050 events, ~47/day). Related defects, deliberately not in this PR:install.ps1runs one scan at enrollment and never registers a recurring task; 55 of 64 zero-tool Windows devices reported exactly once (avg 0.5h lifespan). Largest cohort, ~26 devices whose users have active Copilot traffic.find_claude_binary_for_userhas no machine-global candidates on Windows (noProgram Files/ProgramData) and thewhichbackstop is explicitly skipped, so a system-wide Claude Code install is invisible in every context. Confirmed by a live Windows Server 2022 SYSTEM test.windows/antigravity/antigravity.py:112andwindows/replit/replit.py:213carry this same defect, in a worse form: both usePath.home(), neither referencesself.user_home, and underis_running_as_admin()they union every user's Programs dir into one candidate list whiledetect()returns the first match. Same prod query used above will size it./Applicationswith no~/Applicationsprobe, the same defect class the Cowork fix (WEB-5572: Detect Claude Cowork when Claude Desktop is installed in ~/Applications #246) addressed for one tool.🤖 Generated with Claude Code
https://claude.ai/code/session_01RYVuhiiJ8mW3T1P1soDFLR
Greptile Summary
The PR scopes Windows Cursor and Windsurf per-user installation probes to the profile currently being scanned while retaining current-home fallback behavior and machine-global probes.
user_home.Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Resolve target Windows profile] --> B[Assign detector.user_home] B --> C[Probe target AppData install paths] C --> D[Probe machine-global Program Files paths] D --> E[Attribute detected tool to target profile]Reviews (3): Last reviewed commit: "Make the Cursor/Windsurf scoping tests O..." | Re-trigger Greptile
Context used: