Files
pmg/internal/audit/cloud_sink.go
T
Sahilb315 1276a1ebaa fix: address system-install review findings
- shim: make system executable resolution injectable so tests pass under
  umask 002; skip the root-owner test when running as root
- doctor: treat resolution into either the system or per-user shim dir as
  intercepted, and collapse the shim-in-PATH check to a single call site
- setup: make remove (both --system and per-user) best-effort with
  errors.Join so one failed step no longer strands the other artifact
- shim: allow a group-writable install parent dir (Debian/Ubuntu ship
  /usr/local/bin as root:staff 2775) while still rejecting world-writable
  and non-root-owned parents
- audit: attribute cloud events to SUDO_USER when running under sudo
- docs: drop the soft-fail event-logging claim (hard-fail is retained)
2026-07-14 00:44:44 +05:30

150 lines
3.8 KiB
Go

package audit
import (
"context"
"errors"
"fmt"
"os"
"os/user"
"strings"
controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
"github.com/google/uuid"
"github.com/safedep/dry/cloud/endpointsync"
"github.com/safedep/dry/log"
"github.com/safedep/pmg/config"
)
type cloudSink struct {
*SyncClientBundle
invocationID string
ciResolver CloudSinkCIResolver
command string
workingDir string
}
func newCloudSink(cfg *config.RuntimeConfig, ciResolver CloudSinkCIResolver) (*cloudSink, error) {
bundle, err := NewSyncClientBundle(cfg)
if err != nil {
return nil, err
}
invocationID, err := uuid.NewRandom()
if err != nil {
if closeErr := bundle.Close(); closeErr != nil {
log.Warnf("failed to close sync client bundle after invocation ID failure: %v", closeErr)
}
return nil, fmt.Errorf("failed to generate invocation ID: %w", err)
}
wd, err := os.Getwd()
if err != nil {
if closeErr := bundle.Close(); closeErr != nil {
log.Warnf("failed to close sync client bundle after getwd failure: %v", closeErr)
}
return nil, fmt.Errorf("failed to get working directory: %w", err)
}
return &cloudSink{
SyncClientBundle: bundle,
invocationID: invocationID.String(),
ciResolver: ciResolver,
workingDir: wd,
}, nil
}
func (s *cloudSink) Handle(ctx context.Context, event AuditEvent) error {
if event.Type == EventTypeInstallStarted {
s.command = buildCommand(event.PackageManager, event.Args)
return nil
}
pmgEvents := s.translateToPmgEvents(event)
if len(pmgEvents) == 0 {
return nil
}
for _, pmgEvent := range pmgEvents {
toolEvent, err := s.syncClient.NewEvent()
if err != nil {
return fmt.Errorf("failed to create tool event: %w", err)
}
toolEvent.SetPmgEvent(pmgEvent)
toolEvent.SetInvocationId(s.invocationID)
// Invocation context (CI, command, working dir) is set once per
// execution on the session summary event to avoid redundancy.
if event.Type == EventTypeSessionComplete {
toolEvent.SetInvocationContext(s.buildInvocationContext())
}
if err := s.syncClient.Emit(ctx, toolEvent); err != nil {
if errors.Is(err, endpointsync.ErrWALFull) {
log.Warnf("Cloud sync WAL is full, dropping event: %v", err)
return nil
}
return fmt.Errorf("failed to emit cloud event: %w", err)
}
}
return nil
}
func (s *cloudSink) buildInvocationContext() *controltowerv1.EndpointInvocationContext {
ctx := &controltowerv1.EndpointInvocationContext{}
ctx.SetCommand(s.command)
ctx.SetWorkingDirectory(s.workingDir)
if u := invokingUser(); u != nil {
ctx.SetUsername(u.Username)
ctx.SetUsernameUid(u.Uid)
}
if s.ciResolver != nil {
ci := &controltowerv1.EndpointCIContext{}
ci.SetProvider(s.ciResolver.Provider())
ci.SetRunId(s.ciResolver.RunId())
ci.SetRepository(s.ciResolver.Repository())
ci.SetBranch(s.ciResolver.Branch())
ci.SetCommitSha(s.ciResolver.CommitSha())
ci.SetActor(s.ciResolver.Actor())
ci.SetPrNumber(s.ciResolver.PrNumber())
if metadata := s.ciResolver.Metadata(); len(metadata) > 0 {
ci.SetMetadata(metadata)
}
ctx.SetCi(ci)
}
return ctx
}
// invokingUser resolves the human behind the command, preferring SUDO_USER so a
// `sudo npm ...` is attributed to the operator rather than root.
func invokingUser() *user.User {
if name := os.Getenv("SUDO_USER"); name != "" {
if u, err := user.Lookup(name); err == nil {
return u
}
}
u, err := user.Current()
if err != nil {
return nil
}
return u
}
func buildCommand(packageManager string, args []string) string {
if packageManager == "" {
return ""
}
if len(args) == 0 {
return packageManager
}
return packageManager + " " + strings.Join(args, " ")
}
// Close delegates to the embedded SyncClientBundle.Close().
func (s *cloudSink) Close() error {
return s.SyncClientBundle.Close()
}