Skip to content

Commit 822ce37

Browse files
Merge pull request #2 from shubham-stepsecurity/sm/feat/migrate
Sm/feat/migrate
2 parents 38280a1 + 56de262 commit 822ce37

2 files changed

Lines changed: 79 additions & 16 deletions

File tree

internal/executor/executor_unix.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,52 @@ package executor
55
import (
66
"context"
77
"os"
8+
"runtime"
89
"strings"
910
)
1011

1112
func (r *Real) IsRoot() bool {
1213
return os.Getuid() == 0
1314
}
1415

16+
// resolveUserShell returns the given user's configured login shell on macOS by
17+
// consulting Directory Services (dscl). Returns "" on non-darwin platforms, if
18+
// the lookup fails, or if the resolved path isn't an executable file — in which
19+
// case callers should fall back to /bin/bash.
20+
//
21+
// Mirrors stepsecurity-dev-machine-guard.sh:run_as_logged_in_user. Matters when
22+
// the user's PATH (including npm/pnpm/yarn via nvm/fnm/homebrew) is configured
23+
// only in zsh profile files (.zprofile/.zshrc) — bash -l on such a user sources
24+
// nothing and runs with a stripped PATH, producing empty package scans.
25+
func (r *Real) resolveUserShell(ctx context.Context, username string) string {
26+
if runtime.GOOS != "darwin" || username == "" {
27+
return ""
28+
}
29+
stdout, _, _, err := r.Run(ctx, "dscl", ".", "-read", "/Users/"+username, "UserShell")
30+
if err != nil {
31+
return ""
32+
}
33+
fields := strings.Fields(strings.TrimSpace(stdout))
34+
if len(fields) < 2 {
35+
return ""
36+
}
37+
shell := fields[1]
38+
info, err := os.Stat(shell)
39+
if err != nil || info.IsDir() || info.Mode()&0o111 == 0 {
40+
return ""
41+
}
42+
return shell
43+
}
44+
1545
func (r *Real) RunAsUser(ctx context.Context, username, command string) (string, error) {
1646
if !r.IsRoot() {
1747
stdout, _, _, err := r.Run(ctx, "bash", "-c", command)
1848
return strings.TrimSpace(stdout), err
1949
}
20-
stdout, _, _, err := r.Run(ctx, "sudo", "-H", "-u", username, "bash", "-l", "-c", command)
50+
shell := r.resolveUserShell(ctx, username)
51+
if shell == "" {
52+
shell = "/bin/bash"
53+
}
54+
stdout, _, _, err := r.Run(ctx, "sudo", "-H", "-u", username, shell, "-l", "-c", command)
2155
return strings.TrimSpace(stdout), err
2256
}

internal/telemetry/telemetry.go

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -419,26 +419,55 @@ func uploadToS3(ctx context.Context, log *progress.Logger, payload *Payload) err
419419
return fmt.Errorf("empty upload URL in response")
420420
}
421421

422-
// Upload payload to S3
423-
log.Progress("Uploading telemetry to S3...")
424-
putReq, err := http.NewRequestWithContext(ctx, http.MethodPut, urlResp.UploadURL, bytes.NewReader(payloadJSON))
425-
if err != nil {
426-
return fmt.Errorf("creating S3 PUT request: %w", err)
427-
}
428-
putReq.Header.Set("Content-Type", "application/json")
422+
// Upload payload to S3 with retry — use a longer timeout since payloads
423+
// with npm scan data and execution logs can be several MB.
424+
log.Progress("Uploading telemetry to S3 (%d bytes)...", len(payloadJSON))
425+
s3Client := &http.Client{Timeout: 60 * time.Second}
426+
const maxRetries = 3
427+
var putResp *http.Response
428+
for attempt := 1; attempt <= maxRetries; attempt++ {
429+
uploadStart := time.Now()
430+
putReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPut, urlResp.UploadURL, bytes.NewReader(payloadJSON))
431+
if reqErr != nil {
432+
return fmt.Errorf("creating S3 PUT request: %w", reqErr)
433+
}
434+
putReq.Header.Set("Content-Type", "application/json")
429435

430-
putResp, err := client.Do(putReq)
431-
if err != nil {
432-
return fmt.Errorf("uploading to S3: %w", err)
436+
putResp, err = s3Client.Do(putReq)
437+
elapsed := time.Since(uploadStart)
438+
439+
if err == nil && putResp.StatusCode == http.StatusOK {
440+
log.Progress("Uploaded to S3 in %s", elapsed)
441+
break
442+
}
443+
444+
// Clean up response body before retry
445+
if putResp != nil {
446+
_, _ = io.Copy(io.Discard, putResp.Body)
447+
_ = putResp.Body.Close()
448+
}
449+
450+
if attempt == maxRetries {
451+
if err != nil {
452+
return fmt.Errorf("uploading to S3 (payload: %d bytes, elapsed: %s, attempts: %d): %w",
453+
len(payloadJSON), elapsed, maxRetries, err)
454+
}
455+
return fmt.Errorf("S3 upload failed with status %d (payload: %d bytes, attempts: %d)",
456+
putResp.StatusCode, len(payloadJSON), maxRetries)
457+
}
458+
459+
// Log retry and backoff
460+
backoff := time.Duration(attempt) * 2 * time.Second
461+
if err != nil {
462+
log.Progress("S3 upload attempt %d/%d failed (%s), retrying in %s...", attempt, maxRetries, elapsed, backoff)
463+
} else {
464+
log.Progress("S3 upload attempt %d/%d got status %d, retrying in %s...", attempt, maxRetries, putResp.StatusCode, backoff)
465+
}
466+
time.Sleep(backoff)
433467
}
434468
defer func() { _ = putResp.Body.Close() }()
435469
_, _ = io.Copy(io.Discard, putResp.Body)
436470

437-
if putResp.StatusCode != http.StatusOK {
438-
return fmt.Errorf("S3 upload failed with status %d", putResp.StatusCode)
439-
}
440-
log.Progress("Uploaded to S3")
441-
442471
// Notify backend
443472
log.Progress("Notifying backend of upload...")
444473
notifyBody, _ := json.Marshal(map[string]string{

0 commit comments

Comments
 (0)