]> vilimpoc.org git repositories - dotfiles/commitdiff
dotfiles: give the kernel logger's own ACL to Performance Log Users
authorMax Vilimpoc <max@vilimpoc.org>
Sat, 29 Aug 2026 17:30:32 +0000 (19:30 +0200)
committerMax Vilimpoc <max@vilimpoc.org>
Tue, 1 Sep 2026 10:53:18 +0000 (12:53 +0200)
The group membership and SeSystemProfilePrivilege were necessary and not
sufficient.  Measured on this box after signing in with both in place:

    xperf -start X -on Microsoft-Windows-Kernel-Process   -> exit 0, trace written
    xperf -on base                                        -> NT Kernel Logger:
                                                             Access is denied. (0x5)
    wpr -start GeneralProfile                             -> 0x80070005

The user-mode session proves the group fixed session control, and wpr's error
moving off "Failed to enable the policy to profile system performance" proves
the privilege took.  What is left is the kernel logger itself: it does not use
ETW's default per-GUID descriptor, and the explicit one on
SystemTraceControlGuid does not mention Performance Log Users.
EventAccessQuery on that GUID returns access denied outright from the account,
which is the tell.

So add an ACE for the group with EventAccessControl (EventSecurityAddDACL, so
the entries Windows relies on stay put), carrying the controller rights
including TRACELOG_ACCESS_KERNEL_LOGGER -- the right that names this particular
session.  It goes to the group like the privilege does, keeping membership the
single switch, and the log now records the resulting DACL.

The step moves to the FRONT of the elevated script and gains -EtwRightsOnly,
which runs it and exits.  It is seconds of LSA and registry work, where a full
run is dominated by three Visual Studio passes that take minutes with nothing to
do -- and iterating on this needed a way to apply it without paying for those.
`exit` inside the try still runs the finally, so the transcript is stopped and
the log left readable by the non-elevated caller.

The README now states the cost plainly: a member of that group can capture
system-wide kernel traces, including paths and command lines from every account
on the box.

Exercised under Windows PowerShell 5.1: the script parses, the EtwAcl interop
compiles, the rights mask reads 0x0FE1, and both EventAccessControl and
EventAccessQuery return a clean "access denied" from a non-elevated shell rather
than marshalling garbage.  The grant itself still needs an elevated run.

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

README.md
setup-windows-with-uac.ps1
setup-windows.bat

index aea40620d1cc29d4cb713a6039b6cdbd573edf25..421e1fe4b79768abeadeb317a23289af239a0adb 100644 (file)
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ throwaway VM reachable from a Linux host.
 | --- | --- |
 | `setup-windows.bat` | Entry point. Runs the winget installs, then launches the elevated half and prints its log, then runs the non-elevated script. |
 | `setup-windows-no-uac.ps1` | The non-elevated, per-user half: WinMerge and BinSkim on the user `PATH`, and the global git config (identity, plus `core.sshCommand`). Can also be run directly from an ordinary prompt. |
 | --- | --- |
 | `setup-windows.bat` | Entry point. Runs the winget installs, then launches the elevated half and prints its log, then runs the non-elevated script. |
 | `setup-windows-no-uac.ps1` | The non-elevated, per-user half: WinMerge and BinSkim on the user `PATH`, and the global git config (identity, plus `core.sshCommand`). Can also be run directly from an ordinary prompt. |
-| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs the OpenSSH Client and Server capabilities and starts `sshd`, unpacks the `rsync-windows` release zip for this architecture (`rsync.exe` plus the `ssh.exe` it runs) into `C:\Tools\rsync` on the machine `PATH`, then installs Visual Studio 2022 Community with the required components, the WDK, and the Windows Performance Toolkit, and finally grants one ordinary account the rights to collect ETW traces without elevation. Can also be run directly from an Administrator prompt — pass `-TraceUser DOMAIN\user` to say who gets those rights. |
+| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs the OpenSSH Client and Server capabilities and starts `sshd`, unpacks the `rsync-windows` release zip for this architecture (`rsync.exe` plus the `ssh.exe` it runs) into `C:\Tools\rsync` on the machine `PATH`, then installs Visual Studio 2022 Community with the required components, the WDK, and the Windows Performance Toolkit, and finally grants one ordinary account the rights to collect ETW traces without elevation. Can also be run directly from an Administrator prompt — pass `-TraceUser DOMAIN\user` to say who gets those rights, and `-EtwRightsOnly` to do that step alone. |
 | `setup-windows-7-test-env.bat` | Prepares a **Windows 7 VM** as a test target driven from the host by `VBoxManage guestcontrol`. Copy it into the guest and run it there; it is idempotent, so re-run it after any snapshot restore. The per-user half needs no UAC (crash-dialog suppression, no screen blanking, a staging directory, the shared folder on `Z:`); the machine-wide half is skipped with a notice unless run elevated inside the guest. It then reports what the box can actually test: DWM composition, printers, audio capture devices. |
 
 ## Usage
 | `setup-windows-7-test-env.bat` | Prepares a **Windows 7 VM** as a test target driven from the host by `VBoxManage guestcontrol`. Copy it into the guest and run it there; it is idempotent, so re-run it after any snapshot restore. The per-user half needs no UAC (crash-dialog suppression, no screen blanking, a staging directory, the shared folder on `Z:`); the machine-wide half is skipped with a notice unless run elevated inside the guest. It then reports what the box can actually test: DWM composition, printers, audio capture devices. |
 
 ## Usage
@@ -145,27 +145,45 @@ throwaway VM reachable from a Linux host.
   wpr -start GeneralProfile  -> Failed to enable the policy to profile system performance.
   ```
 
   wpr -start GeneralProfile  -> Failed to enable the policy to profile system performance.
   ```
 
-  Creating or controlling *any* ETW session — even a user-mode one naming a
-  single provider — is checked against the security descriptor ETW keeps per
-  provider GUID under `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`, whose
-  default grants the session-control rights to SYSTEM, Administrators, the
-  service accounts and `BUILTIN\Performance Log Users`, and to nobody else.
-  Switching on the *kernel* provider on top of that additionally needs the
-  `SeSystemProfilePrivilege` user right ("Profile system performance"), held by
-  default only by Administrators and `NT SERVICE\WdiServiceHost` — that is the
-  one `wpr` names. So the elevated half grants the privilege to the **group** and
-  puts the account in the group; enabling another account afterwards is just
-  `net localgroup "Performance Log Users" <user> /add`. `SeDebugPrivilege` is
-  deliberately *not* granted: CPU sampling and stack walks of your own processes
-  do not need it, and it is equivalent to handing out administrator.
-
-  Two consequences worth knowing. Both a privilege and a group membership are
+  Three separate things are in the way, and all three have to be dealt with:
+
+  1. Creating or controlling *any* ETW session — even a user-mode one naming a
+     single provider — is checked against the security descriptor ETW keeps per
+     provider GUID under `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`,
+     whose **default** grants the session-control rights to SYSTEM,
+     Administrators, the service accounts and `BUILTIN\Performance Log Users`,
+     and to nobody else.
+  2. Switching on the *kernel* provider needs the `SeSystemProfilePrivilege` user
+     right ("Profile system performance"), held by default only by
+     Administrators and `NT SERVICE\WdiServiceHost` — that is the one `wpr`
+     names.
+  3. The kernel logger is not covered by that default descriptor.
+     `SystemTraceControlGuid` — the session both `xperf -on` and `wpr` drive —
+     carries an explicit one that does not mention Performance Log Users. With
+     1 and 2 in place a user-mode session starts and the privilege is held, and
+     `xperf -on base` *still* answers "NT Kernel Logger: Access is denied" while
+     `wpr`'s error changes to a bare `0x80070005`; even reading that descriptor
+     comes back access-denied, which is the tell. So an ACE for the group is
+     added with `EventAccessControl`, `TRACELOG_ACCESS_KERNEL_LOGGER` included.
+
+  The privilege and the ACE both go to the **group**, and the account then goes
+  into the group, so membership alone is the switch and enabling another account
+  afterwards is just `net localgroup "Performance Log Users" <user> /add`.
+  `SeDebugPrivilege` is deliberately *not* granted: CPU sampling and stack walks
+  of your own processes do not need it, and it is equivalent to handing out
+  administrator. Worth being clear about the cost: a member of that group can
+  capture system-wide kernel traces — process, image, file and registry activity
+  across every account on the box, paths and command lines included.
+
+  Two consequences worth knowing. A privilege and a group membership are both
   read into the access token **at logon**, so the account must sign out and back
   read into the access token **at logon**, so the account must sign out and back
-  in — any new logon does it, and an `ssh` login into the box is the quick way to
-  check without dropping the desktop. And this only helps a **non-admin**
-  account: UAC hands an administrator a filtered token carrying just five
-  harmless privileges, so an admin's ordinary shell still cannot trace however
-  the policy reads. Verify from the target account, unelevated:
+  in for 1 and 2 — any new logon does it, and an `ssh` login into the box is the
+  quick way to check without dropping the desktop. (The ACE in 3 is machine
+  state, read when a session starts, so it applies immediately.) And this only
+  helps a **non-admin** account: UAC hands an administrator a filtered token
+  carrying just five harmless privileges, so an admin's ordinary shell still
+  cannot trace however the policy reads. Verify from the target account,
+  unelevated:
 
   ```powershell
   whoami /priv | findstr SeSystemProfilePrivilege
 
   ```powershell
   whoami /priv | findstr SeSystemProfilePrivilege
@@ -174,6 +192,16 @@ throwaway VM reachable from a Linux host.
 
   Analysis never needed any of this — `wpa.exe` opens an existing `.etl` as a
   plain user. This is only about collection.
 
   Analysis never needed any of this — `wpa.exe` opens an existing `.etl` as a
   plain user. This is only about collection.
+
+  The rights step runs **first** in the elevated half, and `-EtwRightsOnly` runs
+  it and nothing else. It is seconds of LSA and registry work where a full run is
+  dominated by the three Visual Studio passes, which take minutes even with
+  nothing to do:
+
+  ```powershell
+  Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass',
+      '-File','<repo>\setup-windows-with-uac.ps1','-TraceUser','DOMAIN\user','-EtwRightsOnly'
+  ```
 - The scripts were extracted from a native Windows project, so the component
   selection is tuned for that: Spectre-mitigated runtimes, the v141/XP toolset,
   and driver-kit headers. Trim the component lists in the `.ps1` if you don't
 - The scripts were extracted from a native Windows project, so the component
   selection is tuned for that: Spectre-mitigated runtimes, the v141/XP toolset,
   and driver-kit headers. Trim the component lists in the `.ps1` if you don't
index ff947c6ce0c4a9453bad3f7e04a9a6461ae1303b..a546a0a7dc9d2cf44dccdea995f0665b6503903e 100644 (file)
@@ -36,7 +36,14 @@ param(
     #\r
     # Pass an empty string to skip the group membership (the user right is still\r
     # granted to the group, so adding an account later is one command).\r
     #\r
     # Pass an empty string to skip the group membership (the user right is still\r
     # granted to the group, so adding an account later is one command).\r
-    [string] $TraceUser = ''\r
+    [string] $TraceUser = '',\r
+\r
+    # Do the ETW rights step and nothing else. That step is seconds of registry\r
+    # and LSA work with no downloads, where a full run is dominated by the three\r
+    # Visual Studio passes, which take minutes even when they have nothing to do.\r
+    # It is why the ETW step runs FIRST: -EtwRightsOnly is then just an early\r
+    # exit rather than a set of guards down the rest of the script.\r
+    [switch] $EtwRightsOnly\r
 )\r
 \r
 $ErrorActionPreference = 'Stop'\r
 )\r
 \r
 $ErrorActionPreference = 'Stop'\r
@@ -204,6 +211,66 @@ function Grant-AccountRight([string]$Sid, [string]$Right) {
     [LsaRights]::Add((Get-SidBytes $Sid), $Right)\r
 }\r
 \r
     [LsaRights]::Add((Get-SidBytes $Sid), $Right)\r
 }\r
 \r
+# ---------------------------------------------------------------------------\r
+# ETW provider-GUID access control\r
+#\r
+# ETW keeps a security descriptor per provider GUID under\r
+# HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security, and EventAccessControl is\r
+# the documented way to edit one. Editing the registry value directly would work\r
+# too - it is a self-relative SD in a REG_BINARY - but the API takes the SID and\r
+# the rights mask and leaves the descriptor's shape to Windows.\r
+# ---------------------------------------------------------------------------\r
+function Initialize-EtwAclType {\r
+    if ('EtwAcl' -as [type]) { return }\r
+    Add-Type -TypeDefinition @'\r
+using System;\r
+using System.Runtime.InteropServices;\r
+\r
+public static class EtwAcl\r
+{\r
+    // ULONG EventAccessControl(LPGUID, ULONG Operation, PSID, ULONG Rights, BOOLEAN AllowOrDeny)\r
+    [DllImport("advapi32.dll", SetLastError = true)]\r
+    public static extern uint EventAccessControl(ref Guid guid, uint operation, byte[] sid,\r
+        uint rights, [MarshalAs(UnmanagedType.U1)] bool allowOrDeny);\r
+\r
+    // ULONG EventAccessQuery(LPGUID, PSECURITY_DESCRIPTOR, PULONG BufferSize)\r
+    [DllImport("advapi32.dll", SetLastError = true)]\r
+    public static extern uint EventAccessQuery(ref Guid guid, byte[] buffer, ref uint bufferSize);\r
+}\r
+'@\r
+}\r
+\r
+# The rights a session controller needs, from evntrace.h:\r
+#   0x0001 WMIGUID_QUERY               0x0100 TRACELOG_ACCESS_KERNEL_LOGGER\r
+#   0x0020 TRACELOG_CREATE_REALTIME    0x0200 TRACELOG_LOG_EVENT\r
+#   0x0040 TRACELOG_CREATE_ONDISK      0x0400 TRACELOG_ACCESS_REALTIME\r
+#   0x0080 TRACELOG_GUID_ENABLE        0x0800 TRACELOG_REGISTER_GUIDS\r
+# TRACELOG_ACCESS_KERNEL_LOGGER is the one that names the NT Kernel Logger\r
+# specifically; the rest are what any controller needs to create a session,\r
+# write it to disk and enable providers on it.\r
+$EtwControllerRights = 0x0FE1\r
+\r
+function Grant-EtwGuidAccess([string]$Guid, [string]$Sid, [uint32]$Rights) {\r
+    Initialize-EtwAclType\r
+    $g = [Guid]$Guid\r
+    # Operation 2 = EventSecurityAddDACL: add one ACE and leave every existing\r
+    # one in place. EventSecuritySetDACL (0) would REPLACE the descriptor, which\r
+    # on the kernel logger means removing the entries Windows itself relies on.\r
+    $rc = [EtwAcl]::EventAccessControl([ref]$g, 2, (Get-SidBytes $Sid), $Rights, $true)\r
+    if ($rc -ne 0) { throw (New-Object System.ComponentModel.Win32Exception([int]$rc)) }\r
+}\r
+\r
+function Get-EtwGuidSddl([string]$Guid) {\r
+    Initialize-EtwAclType\r
+    $g = [Guid]$Guid\r
+    $size = [uint32]0\r
+    [void][EtwAcl]::EventAccessQuery([ref]$g, $null, [ref]$size)\r
+    if ($size -eq 0) { return $null }\r
+    $buf = New-Object byte[] $size\r
+    if ([EtwAcl]::EventAccessQuery([ref]$g, $buf, [ref]$size) -ne 0) { return $null }\r
+    return (New-Object System.Security.AccessControl.RawSecurityDescriptor($buf, 0)).GetSddlForm('Access')\r
+}\r
+\r
 function Show-VsSetupLogs {\r
     # The VS Installer writes dd_*.log to the invoking user's %TEMP%. Because\r
     # this script runs elevated, that %TEMP% belongs to the elevated user and is\r
 function Show-VsSetupLogs {\r
     # The VS Installer writes dd_*.log to the invoking user's %TEMP%. Because\r
     # this script runs elevated, that %TEMP% belongs to the elevated user and is\r
@@ -273,6 +340,175 @@ try { Start-Transcript -Path $LogFile -Force | Out-Null } catch {}
 \r
 try {\r
 \r
 \r
 try {\r
 \r
+# ---------------------------------------------------------------------------\r
+# ETW collection rights for an ordinary account\r
+#\r
+# Out of the box, xperf and wpr only work elevated. THREE separate things stand\r
+# in a standard user's way, and each has its own error:\r
+#\r
+#     xperf -on base         -> "NT Kernel Logger: Access is denied. (0x5)"\r
+#     wpr -start GeneralProfile\r
+#                            -> "Failed to enable the policy to profile system\r
+#                                performance."  (0xc5585011)\r
+#\r
+# 1. Creating or controlling ANY event tracing session - even a user-mode one\r
+#    naming a single provider - is checked against the security descriptor ETW\r
+#    keeps per provider GUID under\r
+#    HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security. The DEFAULT descriptor\r
+#    grants the session-control rights (TRACELOG_CREATE_ONDISK,\r
+#    TRACELOG_CREATE_REALTIME, TRACELOG_GUID_ENABLE, TRACELOG_LOG_EVENT) to\r
+#    SYSTEM, Administrators, the service accounts, and BUILTIN\Performance Log\r
+#    Users - and to nobody else. That group is the supported hook; its own\r
+#    description says members "may ... enable trace providers, and collect event\r
+#    traces".\r
+#\r
+# 2. Switching on the kernel/system trace provider on top of that needs the\r
+#    SeSystemProfilePrivilege user right ("Profile system performance"), held by\r
+#    default only by Administrators and NT SERVICE\WdiServiceHost. That is the\r
+#    one wpr names in its error.\r
+#\r
+# 3. The kernel logger is not covered by that default descriptor. Its own GUID -\r
+#    SystemTraceControlGuid, the session both `xperf -on` and wpr drive - carries\r
+#    an explicit descriptor that does not mention Performance Log Users, so 1 and\r
+#    2 are not enough by themselves. Measured on this box with both in place: a\r
+#    user-mode session starts (exit 0) and the account holds the privilege, and\r
+#    `xperf -on base` still answers "NT Kernel Logger: Access is denied" while\r
+#    wpr's error changes from the policy message above to a bare 0x80070005.\r
+#    Even READING that descriptor comes back access-denied, which is the tell. So\r
+#    add an ACE for the group with EventAccessControl; TRACELOG_ACCESS_KERNEL_LOGGER\r
+#    is the right that names this particular session.\r
+#\r
+# The privilege and the ACE both go to the GROUP, and the account then goes into\r
+# the group: membership alone becomes the switch, and enabling the next account\r
+# is one `net localgroup` away with no policy or registry edit.\r
+#\r
+# What this costs, stated plainly: a member of that group can capture\r
+# system-wide kernel traces - process, image, file and registry activity across\r
+# every account on the box, paths and command lines included. That is what the\r
+# group is for, and it is the price of collecting a trace without a UAC prompt.\r
+#\r
+# Deliberately NOT granted: SeDebugPrivilege. xperf needs it for neither CPU\r
+# sampling nor walking stacks in your own processes, and it is equivalent to\r
+# handing out administrator.\r
+#\r
+# THIS ONLY HELPS A NON-ADMIN ACCOUNT. Both a privilege and a group membership\r
+# are baked into the access token at LOGON, and UAC hands an administrator a\r
+# filtered token that keeps just five harmless privileges - so an admin's\r
+# ordinary shell still cannot trace, however the policy reads. Running as a\r
+# standard user is what makes this work.\r
+#\r
+# For the same reason 1 and 2 do not take effect in an already-open session: the\r
+# account has to sign out and back in. Any NEW logon does it - an ssh login into\r
+# this box is one, which is the quick way to check without dropping the desktop.\r
+# The ACE in 3 is machine state, read when a session is started, so that one\r
+# applies immediately.\r
+#\r
+# Analysis never needed any of this: wpa.exe opens an existing .etl as a plain\r
+# user. This step is only about collection.\r
+# ---------------------------------------------------------------------------\r
+Write-Step 'ETW collection rights (non-elevated xperf / wpr)'\r
+$PerfLogUsersSid = 'S-1-5-32-559'   # BUILTIN\Performance Log Users\r
+# SystemTraceControlGuid: the NT Kernel Logger / system session that xperf -on\r
+# and wpr both drive. Fixed by contract, from evntrace.h.\r
+$SystemTraceControlGuid = '9e814aad-3204-11d2-9a82-006008a86939'\r
+try {\r
+    # --- The user right, granted to the group ---\r
+    $existing = Get-AccountRight $PerfLogUsersSid\r
+    if ($existing -contains 'SeSystemProfilePrivilege') {\r
+        Write-Host '    OK: Performance Log Users already holds SeSystemProfilePrivilege'\r
+    } else {\r
+        Grant-AccountRight $PerfLogUsersSid 'SeSystemProfilePrivilege'\r
+        Write-Host '    Granted SeSystemProfilePrivilege ("Profile system performance") to Performance Log Users'\r
+    }\r
+\r
+    # --- The kernel logger's own descriptor ---\r
+    # Idempotent: adding the ACE for a SID that already has it rewrites the same\r
+    # entry. Kept in its own try so that a failure here still leaves the group\r
+    # membership below to be done - user-mode sessions work without it.\r
+    try {\r
+        Grant-EtwGuidAccess $SystemTraceControlGuid $PerfLogUsersSid $EtwControllerRights\r
+        Write-Host ("    Granted Performance Log Users the controller rights (0x{0:X4}, TRACELOG_ACCESS_KERNEL_LOGGER included) on SystemTraceControlGuid" -f $EtwControllerRights)\r
+        $sddl = Get-EtwGuidSddl $SystemTraceControlGuid\r
+        if ($sddl) { Write-Host "    kernel logger DACL is now $sddl" -ForegroundColor DarkGray }\r
+    } catch {\r
+        Write-Warning "Could not add the ACE on SystemTraceControlGuid: $($_.Exception.Message)"\r
+        Write-Warning 'xperf -on will keep answering "NT Kernel Logger: Access is denied."'\r
+    }\r
+\r
+    # --- The membership ---\r
+    # Fall back to the console user when the caller did not name one: with\r
+    # over-the-shoulder elevation that is the person who started\r
+    # setup-windows.bat, which is who wants to trace.\r
+    $target = $TraceUser\r
+    if (-not $target) {\r
+        $target = (Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue).UserName\r
+        if ($target) { Write-Host "    No -TraceUser given; using the console user $target" }\r
+    }\r
+\r
+    if (-not $target) {\r
+        Write-Warning 'No account to add to Performance Log Users (pass -TraceUser DOMAIN\user).'\r
+        Write-Warning 'The user right is in place, so this is the only step left:'\r
+        Write-Warning '    net localgroup "Performance Log Users" DOMAIN\user /add'\r
+    } else {\r
+        # Resolve to a SID first: it validates the name, and it is what the\r
+        # membership check compares, so a member spelled ".\claude" in one place\r
+        # and "LATISLAB\claude" in another is still recognised as the same account.\r
+        $targetSid = (New-Object System.Security.Principal.NTAccount($target)).Translate(\r
+                        [System.Security.Principal.SecurityIdentifier])\r
+\r
+        # By SID, never by name: "Performance Log Users" is localised, and\r
+        # Get-LocalGroup -SID is how this stays correct on a non-English box.\r
+        $group = Get-LocalGroup -SID $PerfLogUsersSid\r
+\r
+        # Get-LocalGroupMember throws on a group holding a SID that no longer\r
+        # resolves (a known Windows 10 bug), so a failure to READ the membership\r
+        # must not stop us from writing it - fall through and let the add report.\r
+        $already = $false\r
+        try {\r
+            $already = @(Get-LocalGroupMember -SID $PerfLogUsersSid |\r
+                         Where-Object { $_.SID.Value -eq $targetSid.Value }).Count -gt 0\r
+        } catch {\r
+            Write-Host "    (could not enumerate $($group.Name) members: $($_.Exception.Message))" -ForegroundColor DarkGray\r
+        }\r
+\r
+        if ($already) {\r
+            Write-Host "    OK: $target is already in $($group.Name)"\r
+        } else {\r
+            try {\r
+                Add-LocalGroupMember -SID $PerfLogUsersSid -Member $targetSid.Value\r
+            } catch {\r
+                # "already a member" is only reachable when the enumeration above\r
+                # failed, and is not an error. Matched on the type NAME rather\r
+                # than in a typed catch clause: catch types are resolved when the\r
+                # script is PARSED, before the LocalAccounts module has been\r
+                # autoloaded, so naming the type there is a parse error that\r
+                # would take the whole script down.\r
+                if ($_.Exception.GetType().Name -ne 'MemberExistsException') { throw }\r
+            }\r
+            Write-Host "    Added $target to $($group.Name)"\r
+        }\r
+\r
+        Write-Host ''\r
+        Write-Host "    $target must sign out and back in before this takes effect." -ForegroundColor Yellow\r
+        Write-Host '    Then, from that account (NOT elevated):' -ForegroundColor Yellow\r
+        Write-Host '        whoami /priv | findstr SeSystemProfilePrivilege' -ForegroundColor Yellow\r
+        Write-Host '        xperf -on base ; xperf -stop C:\Temp\trace.etl' -ForegroundColor Yellow\r
+    }\r
+} catch {\r
+    Write-Warning "ETW rights setup failed: $($_.Exception.Message)"\r
+    Write-Warning 'Grant them by hand: secpol.msc > Local Policies > User Rights Assignment >'\r
+    Write-Warning '"Profile system performance" > add Performance Log Users, then'\r
+    Write-Warning '    net localgroup "Performance Log Users" <user> /add'\r
+}\r
+\r
+if ($EtwRightsOnly) {\r
+    # `exit` inside the try still runs the finally below, so the transcript is\r
+    # stopped and the log is left readable by the non-elevated caller.\r
+    Write-Host "`n-EtwRightsOnly: skipping the installs." -ForegroundColor Green\r
+    exit 0\r
+}\r
+\r
+\r
 # ---------------------------------------------------------------------------\r
 # Base tools via winget\r
 # ---------------------------------------------------------------------------\r
 # ---------------------------------------------------------------------------\r
 # Base tools via winget\r
 # ---------------------------------------------------------------------------\r
@@ -695,130 +931,6 @@ if ($WptDir) {
     }\r
 }\r
 \r
     }\r
 }\r
 \r
-# ---------------------------------------------------------------------------\r
-# ETW collection rights for an ordinary account\r
-#\r
-# Out of the box, xperf and wpr only work elevated, and they fail in two\r
-# different ways for a standard user - because two different things are missing:\r
-#\r
-#     xperf -on base         -> "NT Kernel Logger: Access is denied. (0x5)"\r
-#     wpr -start GeneralProfile\r
-#                            -> "Failed to enable the policy to profile system\r
-#                                performance."  (0xc5585011)\r
-#\r
-# 1. Creating or controlling ANY event tracing session - even a user-mode one\r
-#    naming a single provider - is checked against the security descriptor ETW\r
-#    keeps per provider GUID under\r
-#    HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security. The default grants the\r
-#    session-control rights (TRACELOG_CREATE_ONDISK, TRACELOG_CREATE_REALTIME,\r
-#    TRACELOG_GUID_ENABLE, TRACELOG_LOG_EVENT) to SYSTEM, Administrators, the\r
-#    service accounts, and BUILTIN\Performance Log Users - and to nobody else.\r
-#    That group is the supported hook; its own description says members "may\r
-#    ... enable trace providers, and collect event traces".\r
-#\r
-# 2. Switching on the kernel/system trace provider on top of that needs the\r
-#    SeSystemProfilePrivilege user right ("Profile system performance"), held by\r
-#    default only by Administrators and NT SERVICE\WdiServiceHost. That is the\r
-#    one wpr names in its error, and the one xperf trips over for -on base.\r
-#\r
-# So grant the privilege to the GROUP and then put the account in the group:\r
-# membership alone becomes the switch, and enabling the next account is one\r
-# `net localgroup` away with no policy edit.\r
-#\r
-# Deliberately NOT granted: SeDebugPrivilege. xperf needs it for neither CPU\r
-# sampling nor walking stacks in your own processes, and it is equivalent to\r
-# handing out administrator.\r
-#\r
-# THIS ONLY HELPS A NON-ADMIN ACCOUNT. Both a privilege and a group membership\r
-# are baked into the access token at LOGON, and UAC hands an administrator a\r
-# filtered token that keeps just five harmless privileges - so an admin's\r
-# ordinary shell still cannot trace, however the policy reads. Running as a\r
-# standard user is what makes this work.\r
-#\r
-# For the same reason nothing here takes effect in an already-open session: the\r
-# account has to sign out and back in. Any NEW logon does it - an ssh login into\r
-# this box is one, which is the quick way to check without dropping the desktop.\r
-#\r
-# Analysis never needed any of this: wpa.exe opens an existing .etl as a plain\r
-# user. This step is only about collection.\r
-# ---------------------------------------------------------------------------\r
-Write-Step 'ETW collection rights (non-elevated xperf / wpr)'\r
-$PerfLogUsersSid = 'S-1-5-32-559'   # BUILTIN\Performance Log Users\r
-try {\r
-    # --- The user right, granted to the group ---\r
-    $existing = Get-AccountRight $PerfLogUsersSid\r
-    if ($existing -contains 'SeSystemProfilePrivilege') {\r
-        Write-Host '    OK: Performance Log Users already holds SeSystemProfilePrivilege'\r
-    } else {\r
-        Grant-AccountRight $PerfLogUsersSid 'SeSystemProfilePrivilege'\r
-        Write-Host '    Granted SeSystemProfilePrivilege ("Profile system performance") to Performance Log Users'\r
-    }\r
-\r
-    # --- The membership ---\r
-    # Fall back to the console user when the caller did not name one: with\r
-    # over-the-shoulder elevation that is the person who started\r
-    # setup-windows.bat, which is who wants to trace.\r
-    $target = $TraceUser\r
-    if (-not $target) {\r
-        $target = (Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue).UserName\r
-        if ($target) { Write-Host "    No -TraceUser given; using the console user $target" }\r
-    }\r
-\r
-    if (-not $target) {\r
-        Write-Warning 'No account to add to Performance Log Users (pass -TraceUser DOMAIN\user).'\r
-        Write-Warning 'The user right is in place, so this is the only step left:'\r
-        Write-Warning '    net localgroup "Performance Log Users" DOMAIN\user /add'\r
-    } else {\r
-        # Resolve to a SID first: it validates the name, and it is what the\r
-        # membership check compares, so a member spelled ".\claude" in one place\r
-        # and "LATISLAB\claude" in another is still recognised as the same account.\r
-        $targetSid = (New-Object System.Security.Principal.NTAccount($target)).Translate(\r
-                        [System.Security.Principal.SecurityIdentifier])\r
-\r
-        # By SID, never by name: "Performance Log Users" is localised, and\r
-        # Get-LocalGroup -SID is how this stays correct on a non-English box.\r
-        $group = Get-LocalGroup -SID $PerfLogUsersSid\r
-\r
-        # Get-LocalGroupMember throws on a group holding a SID that no longer\r
-        # resolves (a known Windows 10 bug), so a failure to READ the membership\r
-        # must not stop us from writing it - fall through and let the add report.\r
-        $already = $false\r
-        try {\r
-            $already = @(Get-LocalGroupMember -SID $PerfLogUsersSid |\r
-                         Where-Object { $_.SID.Value -eq $targetSid.Value }).Count -gt 0\r
-        } catch {\r
-            Write-Host "    (could not enumerate $($group.Name) members: $($_.Exception.Message))" -ForegroundColor DarkGray\r
-        }\r
-\r
-        if ($already) {\r
-            Write-Host "    OK: $target is already in $($group.Name)"\r
-        } else {\r
-            try {\r
-                Add-LocalGroupMember -SID $PerfLogUsersSid -Member $targetSid.Value\r
-            } catch {\r
-                # "already a member" is only reachable when the enumeration above\r
-                # failed, and is not an error. Matched on the type NAME rather\r
-                # than in a typed catch clause: catch types are resolved when the\r
-                # script is PARSED, before the LocalAccounts module has been\r
-                # autoloaded, so naming the type there is a parse error that\r
-                # would take the whole script down.\r
-                if ($_.Exception.GetType().Name -ne 'MemberExistsException') { throw }\r
-            }\r
-            Write-Host "    Added $target to $($group.Name)"\r
-        }\r
-\r
-        Write-Host ''\r
-        Write-Host "    $target must sign out and back in before this takes effect." -ForegroundColor Yellow\r
-        Write-Host '    Then, from that account (NOT elevated):' -ForegroundColor Yellow\r
-        Write-Host '        whoami /priv | findstr SeSystemProfilePrivilege' -ForegroundColor Yellow\r
-        Write-Host '        xperf -on base ; xperf -stop C:\Temp\trace.etl' -ForegroundColor Yellow\r
-    }\r
-} catch {\r
-    Write-Warning "ETW rights setup failed: $($_.Exception.Message)"\r
-    Write-Warning 'Grant them by hand: secpol.msc > Local Policies > User Rights Assignment >'\r
-    Write-Warning '"Profile system performance" > add Performance Log Users, then'\r
-    Write-Warning '    net localgroup "Performance Log Users" <user> /add'\r
-}\r
 \r
 # ---------------------------------------------------------------------------\r
 Write-Host "`nAll done." -ForegroundColor Green\r
 \r
 # ---------------------------------------------------------------------------\r
 Write-Host "`nAll done." -ForegroundColor Green\r
index 250d4cbb0b128aa076e48fea67a6e4a5d5d8f648..9dda32e5aafb617a7fad88e1a1f90187c4461d2d 100644 (file)
@@ -6,6 +6,7 @@
 \r
 @rem --- Non-admin (per-user) winget installs ---\r
 winget install Anthropic.ClaudeCode\r
 \r
 @rem --- Non-admin (per-user) winget installs ---\r
 winget install Anthropic.ClaudeCode\r
+winget install Brave.Brave\r
 winget install Git.Git\r
 winget install Microsoft.DotNet.SDK.10\r
 winget install Microsoft.PowerShell Microsoft.Sysinternals.ProcessExplorer Microsoft.Sysinternals.ProcessMonitor Microsoft.Sysinternals.SDelete Microsoft.VisualStudioCode Microsoft.WindowsTerminal\r
 winget install Git.Git\r
 winget install Microsoft.DotNet.SDK.10\r
 winget install Microsoft.PowerShell Microsoft.Sysinternals.ProcessExplorer Microsoft.Sysinternals.ProcessMonitor Microsoft.Sysinternals.SDelete Microsoft.VisualStudioCode Microsoft.WindowsTerminal\r