From c8b6d63cac70ea509a3f613377d78a8403379b29 Mon Sep 17 00:00:00 2001 From: Swarit Pandey Date: Thu, 21 May 2026 07:05:29 +0530 Subject: [PATCH 1/2] fix(windows): run scheduled task as logged-in user via /ru INTERACTIVE Signed-off-by: Swarit Pandey --- internal/schtasks/schtasks.go | 8 +++++++- internal/schtasks/schtasks_test.go | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/schtasks/schtasks.go b/internal/schtasks/schtasks.go index 9d7e97d..7cf2f60 100644 --- a/internal/schtasks/schtasks.go +++ b/internal/schtasks/schtasks.go @@ -101,7 +101,13 @@ func buildCreateArgs(binaryPath, logDir string, hours int, isAdmin bool) []strin args := []string{"/create", "/tn", taskName, "/tr", taskCmd, "/sc", "HOURLY", "/mo", strconv.Itoa(hours), "/f"} if isAdmin { - args = append(args, "/ru", "SYSTEM") + // /ru INTERACTIVE binds the task to the BUILTIN\INTERACTIVE group + // (SID S-1-5-4) so it fires under the security context of whoever + // is interactively logged on at trigger time — picking up their + // HKCU, %USERPROFILE%, and PATH. /ru SYSTEM would run as + // NT AUTHORITY\SYSTEM, which can't see any of the user-scoped + // data the scanner depends on. + args = append(args, "/ru", "INTERACTIVE") } return args } diff --git a/internal/schtasks/schtasks_test.go b/internal/schtasks/schtasks_test.go index ce6bf86..34afc11 100644 --- a/internal/schtasks/schtasks_test.go +++ b/internal/schtasks/schtasks_test.go @@ -122,13 +122,13 @@ func TestBuildCreateArgs_Admin(t *testing.T) { for i, a := range args { if a == "/ru" && i+1 < len(args) { foundRU = true - if args[i+1] != "SYSTEM" { - t.Errorf("expected /ru SYSTEM, got /ru %s", args[i+1]) + if args[i+1] != "INTERACTIVE" { + t.Errorf("expected /ru INTERACTIVE, got /ru %s", args[i+1]) } } } if !foundRU { - t.Error("expected /ru SYSTEM for admin install") + t.Error("expected /ru INTERACTIVE for admin install") } } From 10f2422f8a3e3c9a043291e884c71ea7c48fa9c1 Mon Sep 17 00:00:00 2001 From: Swarit Pandey Date: Thu, 21 May 2026 12:39:47 +0530 Subject: [PATCH 2/2] fix(windows): grant Users modify ACL on ProgramData log dir; correct comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /ru INTERACTIVE scheduled task fires under whatever user is logged on at trigger time, typically a non-admin developer. The default ACLs on C:\ProgramData\StepSecurity (inherited from C:\ProgramData) only grant non-admin users Read & Execute on existing files, so cmd.exe's `>>` redirect to agent.log would fail with Access Denied — and a failed redirect aborts the whole task action, so the periodic scan never runs. Grant BUILTIN\Users (SID 545) Modify rights on the log dir after creating it, propagated to files and subfolders, so any logged-in user can append. Also corrects the /ru INTERACTIVE comment: SID S-1-5-4 is in the NT AUTHORITY domain, not BUILTIN (BUILTIN\* SIDs are S-1-5-32-*). --- internal/schtasks/schtasks.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/internal/schtasks/schtasks.go b/internal/schtasks/schtasks.go index 7cf2f60..032b74c 100644 --- a/internal/schtasks/schtasks.go +++ b/internal/schtasks/schtasks.go @@ -42,6 +42,21 @@ func Install(exec executor.Executor, log *progress.Logger) error { return fmt.Errorf("creating log directory: %w", err) } + // For admin installs the log dir lives at C:\ProgramData\StepSecurity, which + // inherits ACLs from C:\ProgramData and only grants non-admin users + // Read & Execute on the files inside. The /ru INTERACTIVE task fires under + // whatever user is logged on — typically a non-admin developer — and + // cmd.exe's `>>` redirect to agent.log would fail with Access Denied, which + // aborts the whole task action. Grant BUILTIN\Users (SID 545) Modify rights + // on the log dir, propagated to files and subfolders, so any logged-in + // user can append to the log files. + if exec.IsRoot() { + _, _, _, icaclsErr := exec.Run(ctx, "icacls", logDir, "/grant", "*S-1-5-32-545:(OI)(CI)M", "/Q") + if icaclsErr != nil { + log.Warn("could not adjust log dir ACLs (%v) — non-admin users may not be able to write to %s", icaclsErr, logDir) + } + } + args := buildCreateArgs(binaryPath, logDir, hours, exec.IsRoot()) log.Debug("schtasks create: binary=%q log_dir=%q hours=%d is_admin=%v", binaryPath, logDir, hours, exec.IsRoot()) @@ -101,11 +116,11 @@ func buildCreateArgs(binaryPath, logDir string, hours int, isAdmin bool) []strin args := []string{"/create", "/tn", taskName, "/tr", taskCmd, "/sc", "HOURLY", "/mo", strconv.Itoa(hours), "/f"} if isAdmin { - // /ru INTERACTIVE binds the task to the BUILTIN\INTERACTIVE group - // (SID S-1-5-4) so it fires under the security context of whoever - // is interactively logged on at trigger time — picking up their - // HKCU, %USERPROFILE%, and PATH. /ru SYSTEM would run as - // NT AUTHORITY\SYSTEM, which can't see any of the user-scoped + // /ru INTERACTIVE binds the task to the NT AUTHORITY\INTERACTIVE + // well-known group (SID S-1-5-4) so it fires under the security + // context of whoever is interactively logged on at trigger time — + // picking up their HKCU, %USERPROFILE%, and PATH. /ru SYSTEM would + // run as NT AUTHORITY\SYSTEM, which can't see any of the user-scoped // data the scanner depends on. args = append(args, "/ru", "INTERACTIVE") }