mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: Add cloud sync event emit (#212)
* feat: Add cloud sync event emit * fix: Linter fixes * fix: Include malysis metadata in confirmed event * fix: Code review fixes * fix: Emit session complet event * fix: Code review fixes * chore: Add comment * chore: Add cloud info in setup info command * fix: Proxy flow must call install started * fix: Code review fixes * fix: Code review fixes * Update internal/audit/cloud_sink.go Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> * fix: Code review fixes --------- Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
0355a5d4fd
commit
e67735c1c3
+70
-4
@@ -3,8 +3,13 @@ package audit
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/internal/analytics"
|
||||
"github.com/safedep/pmg/usefulerror"
|
||||
)
|
||||
|
||||
var global *auditor
|
||||
@@ -17,9 +22,30 @@ func resetGlobal() {
|
||||
global = nil
|
||||
}
|
||||
|
||||
func Initialize() error {
|
||||
a := newAuditor(newEventlogSink())
|
||||
global = a
|
||||
// Initialize sets up the audit system with an eventlog sink and, when enabled,
|
||||
// a cloud sync sink.
|
||||
func Initialize(cfg *config.RuntimeConfig) error {
|
||||
var sinks []Sink
|
||||
sinks = append(sinks, newEventlogSink())
|
||||
|
||||
if cfg.Config.Cloud.Enabled && !analytics.IsDisabled() {
|
||||
cs, err := newCloudSink(cfg)
|
||||
if err != nil {
|
||||
return usefulerror.Useful().
|
||||
Wrap(err).
|
||||
WithCode(usefulerror.ErrCodeLifecycle).
|
||||
WithHumanError("Cloud sync is enabled but failed to initialize").
|
||||
WithHelp("Ensure SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables are set").
|
||||
WithAdditionalHelp("Disable cloud sync with 'cloud.enabled: false' in config if not needed")
|
||||
}
|
||||
sinks = append(sinks, cs)
|
||||
}
|
||||
|
||||
if cfg.Config.Cloud.Enabled && analytics.IsDisabled() {
|
||||
log.Warnf("Cloud sync is disabled because telemetry is disabled")
|
||||
}
|
||||
|
||||
setGlobal(newAuditor(sinks...))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -84,11 +110,14 @@ func LogMalwareBlocked(pv *packagev1.PackageVersion, reason, analysisID, referen
|
||||
}
|
||||
|
||||
// LogMalwareConfirmed records that the user confirmed installation of a flagged package.
|
||||
func LogMalwareConfirmed(pv *packagev1.PackageVersion) {
|
||||
func LogMalwareConfirmed(pv *packagev1.PackageVersion, analysisID string, isMalware, isVerified bool) {
|
||||
logEvent(AuditEvent{
|
||||
Type: EventTypeMalwareConfirmed,
|
||||
Message: fmt.Sprintf("User confirmed installation of flagged package: %s@%s", pkgName(pv), pkgVersion(pv)),
|
||||
PackageVersion: pv,
|
||||
AnalysisID: analysisID,
|
||||
IsMalware: isMalware,
|
||||
IsVerified: isVerified,
|
||||
})
|
||||
|
||||
if global != nil {
|
||||
@@ -206,6 +235,43 @@ func LogError(message string, err error) {
|
||||
logEvent(event)
|
||||
}
|
||||
|
||||
// LogSessionComplete records the end of a PMG invocation with aggregate session stats.
|
||||
func LogSessionComplete(outcome Outcome, flowType FlowType) {
|
||||
if global == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s := global.getSession()
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
cfg := config.Get()
|
||||
|
||||
logEvent(AuditEvent{
|
||||
Type: EventTypeSessionComplete,
|
||||
Message: fmt.Sprintf("Session complete: %s", outcome),
|
||||
SessionData: &SessionData{
|
||||
PackageManager: s.packageManager,
|
||||
FlowType: flowType,
|
||||
Outcome: outcome,
|
||||
TotalAnalyzed: s.totalAnalyzed,
|
||||
AllowedCount: s.allowedCount,
|
||||
BlockedCount: s.blockedCount,
|
||||
ConfirmedCount: s.confirmedCount,
|
||||
TrustedSkipped: s.trustedSkipped,
|
||||
InsecureBypassed: s.insecureBypassed,
|
||||
Duration: time.Since(s.startTime),
|
||||
SandboxEnabled: cfg.Config.Sandbox.Enabled,
|
||||
ParanoidMode: cfg.Config.Paranoid,
|
||||
TransitiveEnabled: cfg.Config.Transitive,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func mergeDetails(base, extra map[string]interface{}) map[string]interface{} {
|
||||
if base == nil {
|
||||
base = make(map[string]interface{})
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -146,7 +148,7 @@ func TestPublicAPISilentWhenNotInitialized(t *testing.T) {
|
||||
|
||||
// None of these should panic
|
||||
LogMalwareBlocked(nil, "reason", "", "", false, false)
|
||||
LogMalwareConfirmed(nil)
|
||||
LogMalwareConfirmed(nil, "", false, false)
|
||||
LogInstallAllowed(nil, 5)
|
||||
LogInstallTrustedAllowed(nil)
|
||||
LogInstallInsecureBypass(nil)
|
||||
@@ -207,7 +209,7 @@ func TestLogMalwareConfirmedIncrementsSession(t *testing.T) {
|
||||
defer resetGlobal()
|
||||
|
||||
a.startSession("npm", nil)
|
||||
LogMalwareConfirmed(testPackageVersion("pkg", "1.0", "npm"))
|
||||
LogMalwareConfirmed(testPackageVersion("pkg", "1.0", "npm"), "a-1", true, false)
|
||||
|
||||
sess := a.getSession()
|
||||
require.NotNil(t, sess)
|
||||
@@ -229,3 +231,86 @@ func TestLogInstallTrustedAllowedIncrementsSession(t *testing.T) {
|
||||
assert.Equal(t, uint32(1), sess.trustedSkipped)
|
||||
assert.Equal(t, uint32(1), sess.totalAnalyzed)
|
||||
}
|
||||
|
||||
func TestLogSessionCompleteDispatchesEvent(t *testing.T) {
|
||||
s := &mockSink{}
|
||||
a := newAuditor(s)
|
||||
setGlobal(a)
|
||||
defer resetGlobal()
|
||||
|
||||
a.startSession("npm", []string{"install", "express"})
|
||||
LogInstallAllowed(testPackageVersion("express", "4.0.0", "npm"), 1)
|
||||
LogSessionComplete(OutcomeSuccess, FlowTypeGuard)
|
||||
|
||||
events := s.getEvents()
|
||||
require.Len(t, events, 2)
|
||||
assert.Equal(t, EventTypeSessionComplete, events[1].Type)
|
||||
require.NotNil(t, events[1].SessionData)
|
||||
assert.Equal(t, "npm", events[1].SessionData.PackageManager)
|
||||
assert.Equal(t, FlowTypeGuard, events[1].SessionData.FlowType)
|
||||
assert.Equal(t, OutcomeSuccess, events[1].SessionData.Outcome)
|
||||
assert.Equal(t, uint32(1), events[1].SessionData.AllowedCount)
|
||||
}
|
||||
|
||||
func TestLogSessionCompleteSilentWhenNotInitialized(t *testing.T) {
|
||||
resetGlobal()
|
||||
// Should not panic
|
||||
LogSessionComplete(OutcomeSuccess, FlowTypeGuard)
|
||||
}
|
||||
|
||||
// TestUIOutcomesMappToAuditOutcomes ensures every ui.ExecutionOutcome has a
|
||||
// corresponding audit.Outcome constant. If someone adds a new outcome to the
|
||||
// UI layer without updating the audit package, this test will fail.
|
||||
//
|
||||
// Both lists must be kept in sync manually. If a new ui.ExecutionOutcome is
|
||||
// added, add it to uiOutcomes below AND add a matching audit.Outcome constant.
|
||||
// The length check catches the case where one list is updated but not the other.
|
||||
func TestUIOutcomesMappToAuditOutcomes(t *testing.T) {
|
||||
auditOutcomes := []Outcome{
|
||||
OutcomeSuccess,
|
||||
OutcomeBlocked,
|
||||
OutcomeUserCancelled,
|
||||
OutcomeDryRun,
|
||||
OutcomeError,
|
||||
OutcomeInsecureBypass,
|
||||
}
|
||||
|
||||
uiOutcomes := []ui.ExecutionOutcome{
|
||||
ui.OutcomeSuccess,
|
||||
ui.OutcomeBlocked,
|
||||
ui.OutcomeUserCancelled,
|
||||
ui.OutcomeDryRun,
|
||||
ui.OutcomeError,
|
||||
ui.OutcomeInsecureBypass,
|
||||
}
|
||||
|
||||
require.Equal(t, len(uiOutcomes), len(auditOutcomes),
|
||||
"ui.ExecutionOutcome and audit.Outcome count mismatch — a new outcome was added to one but not the other")
|
||||
|
||||
knownOutcomes := make(map[Outcome]bool, len(auditOutcomes))
|
||||
for _, o := range auditOutcomes {
|
||||
knownOutcomes[o] = true
|
||||
}
|
||||
|
||||
for _, uiOutcome := range uiOutcomes {
|
||||
auditOutcome := Outcome(uiOutcome.String())
|
||||
assert.True(t, knownOutcomes[auditOutcome],
|
||||
"ui.ExecutionOutcome %q (String()=%q) has no matching audit.Outcome constant — add it to audit/event.go",
|
||||
uiOutcome, uiOutcome.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitializeWithCloudDisabled(t *testing.T) {
|
||||
resetGlobal()
|
||||
defer resetGlobal()
|
||||
|
||||
cfg := config.Get()
|
||||
cfg.Config.Cloud.Enabled = false
|
||||
|
||||
err := Initialize(cfg)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, global)
|
||||
|
||||
// Should have exactly one sink (eventlog)
|
||||
assert.Len(t, global.sinks, 1)
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@ import (
|
||||
)
|
||||
|
||||
type session struct {
|
||||
mu sync.Mutex
|
||||
startTime time.Time
|
||||
packageManager string
|
||||
args []string
|
||||
totalAnalyzed uint32
|
||||
allowedCount uint32
|
||||
blockedCount uint32
|
||||
confirmedCount uint32
|
||||
trustedSkipped uint32
|
||||
mu sync.Mutex
|
||||
startTime time.Time
|
||||
packageManager string
|
||||
args []string
|
||||
totalAnalyzed uint32
|
||||
allowedCount uint32
|
||||
blockedCount uint32
|
||||
confirmedCount uint32
|
||||
trustedSkipped uint32
|
||||
insecureBypassed uint32
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/safedep/dry/cloud"
|
||||
"github.com/safedep/dry/cloud/endpointsync"
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/config"
|
||||
appVersion "github.com/safedep/pmg/internal/version"
|
||||
)
|
||||
|
||||
type cloudSink struct {
|
||||
syncClient *endpointsync.SyncClient
|
||||
cloudClient *cloud.Client
|
||||
invocationID string
|
||||
}
|
||||
|
||||
func newCloudSink(cfg *config.RuntimeConfig) (*cloudSink, error) {
|
||||
resolver, err := cloud.NewEnvCredentialResolver()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create credential resolver: %w", err)
|
||||
}
|
||||
|
||||
creds, err := resolver.Resolve()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve cloud credentials: %w", err)
|
||||
}
|
||||
|
||||
cloudClient, err := cloud.NewDataPlaneClient("pmg", creds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create data plane client: %w", err)
|
||||
}
|
||||
|
||||
transport := endpointsync.NewGrpcTransport(cloudClient.Connection())
|
||||
|
||||
sink, err := newCloudSinkWithTransport(transport, cfg.Config.Cloud.EndpointID, cfg.CloudSyncDBPath())
|
||||
if err != nil {
|
||||
if closeErr := cloudClient.Close(); closeErr != nil {
|
||||
log.Warnf("failed to close cloud client after sink init failure: %v", closeErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sink.cloudClient = cloudClient
|
||||
return sink, nil
|
||||
}
|
||||
|
||||
func newCloudSinkWithTransport(transport endpointsync.EventTransport, endpointID, walPath string) (*cloudSink, error) {
|
||||
var identityOpts []endpointsync.EndpointIdentityOption
|
||||
if endpointID != "" {
|
||||
identityOpts = append(identityOpts, endpointsync.WithEndpointID(endpointID))
|
||||
}
|
||||
|
||||
identity := endpointsync.NewEndpointIdentityResolver(identityOpts...)
|
||||
|
||||
toolVersion := appVersion.Version
|
||||
if toolVersion == "" {
|
||||
toolVersion = "dev"
|
||||
}
|
||||
|
||||
syncClient, err := endpointsync.NewSyncClient("pmg", toolVersion, transport, identity,
|
||||
endpointsync.WithWALPath(walPath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create sync client: %w", err)
|
||||
}
|
||||
|
||||
invocationID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
if closeErr := syncClient.Close(); closeErr != nil {
|
||||
log.Warnf("failed to close sync client after invocation ID generation failure: %v", closeErr)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to generate invocation ID: %w", err)
|
||||
}
|
||||
|
||||
return &cloudSink{
|
||||
syncClient: syncClient,
|
||||
invocationID: invocationID.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *cloudSink) Handle(ctx context.Context, event AuditEvent) error {
|
||||
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)
|
||||
|
||||
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) Close() error {
|
||||
var errs []error
|
||||
if s.syncClient != nil {
|
||||
if err := s.syncClient.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if s.cloudClient != nil {
|
||||
if err := s.cloudClient.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
servicev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/services/controltower/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockTransport struct {
|
||||
requests []*servicev1.SyncEventsRequest
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (m *mockTransport) Send(_ context.Context, req *servicev1.SyncEventsRequest) (*servicev1.SyncEventsResponse, error) {
|
||||
m.requests = append(m.requests, req)
|
||||
confirmed := make([]string, 0, len(req.GetEvents()))
|
||||
for _, e := range req.GetEvents() {
|
||||
confirmed = append(confirmed, e.GetEventId())
|
||||
}
|
||||
return &servicev1.SyncEventsResponse{ConfirmedEventIds: confirmed}, nil
|
||||
}
|
||||
|
||||
func (m *mockTransport) Close() error {
|
||||
m.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCloudSinkEmitsTranslatableEvents(t *testing.T) {
|
||||
transport := &mockTransport{}
|
||||
walPath := t.TempDir() + "/test-sync.db"
|
||||
|
||||
sink, err := newCloudSinkWithTransport(transport, "", walPath)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, sink.Close())
|
||||
}()
|
||||
|
||||
err = sink.Handle(context.Background(), AuditEvent{
|
||||
Type: EventTypeMalwareBlocked,
|
||||
Timestamp: time.Now(),
|
||||
Message: "blocked malware package",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCloudSinkSkipsUntranslatableEvents(t *testing.T) {
|
||||
transport := &mockTransport{}
|
||||
walPath := t.TempDir() + "/test-sync.db"
|
||||
|
||||
sink, err := newCloudSinkWithTransport(transport, "", walPath)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, sink.Close())
|
||||
}()
|
||||
|
||||
err = sink.Handle(context.Background(), AuditEvent{
|
||||
Type: EventTypeProxyHostObserved,
|
||||
Timestamp: time.Now(),
|
||||
Message: "observed proxy host",
|
||||
Hostname: "example.com",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCloudSinkEmitAndSync(t *testing.T) {
|
||||
transport := &mockTransport{}
|
||||
walPath := t.TempDir() + "/test-sync.db"
|
||||
|
||||
sink, err := newCloudSinkWithTransport(transport, "", walPath)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, sink.Close())
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
err = sink.Handle(ctx, AuditEvent{
|
||||
Type: EventTypeMalwareBlocked,
|
||||
Timestamp: time.Now(),
|
||||
Message: "blocked malware package",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
synced, err := sink.syncClient.Sync(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 1, synced)
|
||||
assert.Equal(t, 1, len(transport.requests))
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
func (s *cloudSink) translateToPmgEvents(event AuditEvent) []*controltowerv1.PmgEvent {
|
||||
switch event.Type {
|
||||
case EventTypeMalwareBlocked:
|
||||
return []*controltowerv1.PmgEvent{newPackageDecisionEvent(event, controltowerv1.PmgPackageAction_PMG_PACKAGE_ACTION_BLOCKED)}
|
||||
case EventTypeMalwareConfirmed:
|
||||
return []*controltowerv1.PmgEvent{newPackageDecisionEvent(event, controltowerv1.PmgPackageAction_PMG_PACKAGE_ACTION_CONFIRMED)}
|
||||
case EventTypeInstallInsecureBypass:
|
||||
// PmgInsecureBypass is a session-level aggregate (package manager + total bypassed count),
|
||||
// not a per-package event. It is emitted as part of EventTypeSessionComplete when
|
||||
// the session's insecureBypassed counter is > 0.
|
||||
return nil
|
||||
case EventTypeSandboxOverride:
|
||||
return []*controltowerv1.PmgEvent{newSandboxOverrideEvent(event)}
|
||||
case EventTypeError:
|
||||
return []*controltowerv1.PmgEvent{newErrorEvent(event)}
|
||||
case EventTypeSessionComplete:
|
||||
if event.SessionData == nil {
|
||||
return nil
|
||||
}
|
||||
events := []*controltowerv1.PmgEvent{newSessionSummaryEvent(event.SessionData)}
|
||||
if event.SessionData.InsecureBypassed > 0 {
|
||||
events = append(events, newInsecureBypassFromSession(event.SessionData))
|
||||
}
|
||||
return events
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func newPackageDecisionEvent(event AuditEvent, action controltowerv1.PmgPackageAction) *controltowerv1.PmgEvent {
|
||||
decision := &controltowerv1.PmgPackageDecision{}
|
||||
decision.SetPackageVersion(event.PackageVersion)
|
||||
decision.SetAction(action)
|
||||
|
||||
if event.AnalysisID != "" {
|
||||
decision.SetAnalysisId(event.AnalysisID)
|
||||
}
|
||||
decision.SetIsMalware(event.IsMalware)
|
||||
decision.SetIsVerified(event.IsVerified)
|
||||
|
||||
e := &controltowerv1.PmgEvent{}
|
||||
e.SetEventType(controltowerv1.PmgEventType_PMG_EVENT_TYPE_PACKAGE_DECISION)
|
||||
e.SetPackageDecision(decision)
|
||||
return e
|
||||
}
|
||||
|
||||
func newSandboxOverrideEvent(event AuditEvent) *controltowerv1.PmgEvent {
|
||||
override := &controltowerv1.PmgSandboxOverride{}
|
||||
override.SetSandboxProfile(event.ProfileName)
|
||||
|
||||
var flattened []string
|
||||
for _, m := range event.Overrides {
|
||||
for k, v := range m {
|
||||
flattened = append(flattened, fmt.Sprintf("%s:%s", k, v))
|
||||
}
|
||||
}
|
||||
override.SetOverrides(flattened)
|
||||
|
||||
e := &controltowerv1.PmgEvent{}
|
||||
e.SetEventType(controltowerv1.PmgEventType_PMG_EVENT_TYPE_SANDBOX_OVERRIDE)
|
||||
e.SetSandboxOverride(override)
|
||||
return e
|
||||
}
|
||||
|
||||
func newErrorEvent(event AuditEvent) *controltowerv1.PmgEvent {
|
||||
pmgErr := &controltowerv1.PmgError{}
|
||||
if event.Error != nil {
|
||||
pmgErr.SetErrorType(fmt.Sprintf("%T", event.Error))
|
||||
}
|
||||
pmgErr.SetMessage(event.Message)
|
||||
|
||||
e := &controltowerv1.PmgEvent{}
|
||||
e.SetEventType(controltowerv1.PmgEventType_PMG_EVENT_TYPE_ERROR)
|
||||
e.SetError(pmgErr)
|
||||
return e
|
||||
}
|
||||
|
||||
func newSessionSummaryEvent(data *SessionData) *controltowerv1.PmgEvent {
|
||||
summary := &controltowerv1.PmgSessionSummary{}
|
||||
summary.SetPackageManager(mapPackageManager(data.PackageManager))
|
||||
summary.SetFlowType(mapFlowType(data.FlowType))
|
||||
summary.SetTotalAnalyzed(data.TotalAnalyzed)
|
||||
summary.SetAllowedCount(data.AllowedCount)
|
||||
summary.SetBlockedCount(data.BlockedCount)
|
||||
summary.SetConfirmedCount(data.ConfirmedCount)
|
||||
summary.SetTrustedSkipped(data.TrustedSkipped)
|
||||
summary.SetDuration(durationpb.New(data.Duration))
|
||||
summary.SetSandboxEnabled(data.SandboxEnabled)
|
||||
summary.SetParanoidMode(data.ParanoidMode)
|
||||
summary.SetTransitiveEnabled(data.TransitiveEnabled)
|
||||
summary.SetOutcome(mapSessionOutcome(data.Outcome))
|
||||
|
||||
e := &controltowerv1.PmgEvent{}
|
||||
e.SetEventType(controltowerv1.PmgEventType_PMG_EVENT_TYPE_SESSION_SUMMARY)
|
||||
e.SetSessionSummary(summary)
|
||||
return e
|
||||
}
|
||||
|
||||
func newInsecureBypassFromSession(data *SessionData) *controltowerv1.PmgEvent {
|
||||
bypass := &controltowerv1.PmgInsecureBypass{}
|
||||
bypass.SetPackageManager(mapPackageManager(data.PackageManager))
|
||||
bypass.SetPackagesBypassed(data.InsecureBypassed)
|
||||
|
||||
e := &controltowerv1.PmgEvent{}
|
||||
e.SetEventType(controltowerv1.PmgEventType_PMG_EVENT_TYPE_INSECURE_BYPASS)
|
||||
e.SetInsecureBypass(bypass)
|
||||
return e
|
||||
}
|
||||
|
||||
func mapFlowType(ft FlowType) controltowerv1.PmgFlowType {
|
||||
switch ft {
|
||||
case FlowTypeGuard:
|
||||
return controltowerv1.PmgFlowType_PMG_FLOW_TYPE_GUARD
|
||||
case FlowTypeProxy:
|
||||
return controltowerv1.PmgFlowType_PMG_FLOW_TYPE_PROXY
|
||||
default:
|
||||
return controltowerv1.PmgFlowType_PMG_FLOW_TYPE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func mapSessionOutcome(outcome Outcome) controltowerv1.PmgSessionOutcome {
|
||||
switch outcome {
|
||||
case OutcomeSuccess:
|
||||
return controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_SUCCESS
|
||||
case OutcomeBlocked:
|
||||
return controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_BLOCKED
|
||||
case OutcomeUserCancelled:
|
||||
return controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_USER_CANCELLED
|
||||
case OutcomeError:
|
||||
return controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_ERROR
|
||||
case OutcomeDryRun:
|
||||
return controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_DRY_RUN
|
||||
case OutcomeInsecureBypass:
|
||||
return controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_INSECURE_BYPASS
|
||||
default:
|
||||
return controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func mapPackageManager(name string) controltowerv1.PmgPackageManager {
|
||||
switch name {
|
||||
case "npm", "npx":
|
||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_NPM
|
||||
case "pnpm", "pnpx":
|
||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PNPM
|
||||
case "yarn":
|
||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_YARN
|
||||
case "bun":
|
||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_BUN
|
||||
case "pip", "pip3":
|
||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PIP
|
||||
case "poetry":
|
||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_POETRY
|
||||
case "uv":
|
||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_UV
|
||||
default:
|
||||
return controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testCloudSink returns a cloudSink with no real SyncClient, suitable for testing translation.
|
||||
var testSink = &cloudSink{invocationID: "test-invocation"}
|
||||
|
||||
func TestTranslateMalwareBlocked(t *testing.T) {
|
||||
event := AuditEvent{
|
||||
Type: EventTypeMalwareBlocked,
|
||||
PackageVersion: testPackageVersion("evil-pkg", "1.2.3", "npm"),
|
||||
AnalysisID: "analysis-123",
|
||||
IsMalware: true,
|
||||
IsVerified: true,
|
||||
}
|
||||
|
||||
results := testSink.translateToPmgEvents(event)
|
||||
require.Len(t, results, 1)
|
||||
result := results[0]
|
||||
|
||||
assert.Equal(t, controltowerv1.PmgEventType_PMG_EVENT_TYPE_PACKAGE_DECISION, result.GetEventType())
|
||||
require.True(t, result.HasPackageDecision())
|
||||
|
||||
decision := result.GetPackageDecision()
|
||||
assert.Equal(t, controltowerv1.PmgPackageAction_PMG_PACKAGE_ACTION_BLOCKED, decision.GetAction())
|
||||
assert.Equal(t, "analysis-123", decision.GetAnalysisId())
|
||||
assert.True(t, decision.GetIsMalware())
|
||||
assert.True(t, decision.GetIsVerified())
|
||||
assert.NotNil(t, decision.GetPackageVersion())
|
||||
}
|
||||
|
||||
func TestTranslateMalwareConfirmed(t *testing.T) {
|
||||
event := AuditEvent{
|
||||
Type: EventTypeMalwareConfirmed,
|
||||
PackageVersion: testPackageVersion("suspect-pkg", "2.0.0", "npm"),
|
||||
AnalysisID: "analysis-456",
|
||||
IsMalware: false,
|
||||
IsVerified: false,
|
||||
}
|
||||
|
||||
results := testSink.translateToPmgEvents(event)
|
||||
require.Len(t, results, 1)
|
||||
result := results[0]
|
||||
|
||||
assert.Equal(t, controltowerv1.PmgEventType_PMG_EVENT_TYPE_PACKAGE_DECISION, result.GetEventType())
|
||||
require.True(t, result.HasPackageDecision())
|
||||
|
||||
decision := result.GetPackageDecision()
|
||||
assert.Equal(t, controltowerv1.PmgPackageAction_PMG_PACKAGE_ACTION_CONFIRMED, decision.GetAction())
|
||||
assert.Equal(t, "analysis-456", decision.GetAnalysisId())
|
||||
assert.False(t, decision.GetIsMalware())
|
||||
assert.False(t, decision.GetIsVerified())
|
||||
assert.False(t, decision.GetIsVerified())
|
||||
}
|
||||
|
||||
func TestTranslateInsecureBypassReturnsEmpty(t *testing.T) {
|
||||
event := AuditEvent{
|
||||
Type: EventTypeInstallInsecureBypass,
|
||||
}
|
||||
|
||||
results := testSink.translateToPmgEvents(event)
|
||||
assert.Empty(t, results)
|
||||
}
|
||||
|
||||
func TestTranslateSandboxOverride(t *testing.T) {
|
||||
event := AuditEvent{
|
||||
Type: EventTypeSandboxOverride,
|
||||
ProfileName: "strict",
|
||||
Overrides: []map[string]string{
|
||||
{"read": "/tmp"},
|
||||
{"write": "/var/log"},
|
||||
},
|
||||
}
|
||||
|
||||
results := testSink.translateToPmgEvents(event)
|
||||
require.Len(t, results, 1)
|
||||
result := results[0]
|
||||
|
||||
assert.Equal(t, controltowerv1.PmgEventType_PMG_EVENT_TYPE_SANDBOX_OVERRIDE, result.GetEventType())
|
||||
require.True(t, result.HasSandboxOverride())
|
||||
|
||||
override := result.GetSandboxOverride()
|
||||
assert.Equal(t, "strict", override.GetSandboxProfile())
|
||||
assert.ElementsMatch(t, []string{"read:/tmp", "write:/var/log"}, override.GetOverrides())
|
||||
}
|
||||
|
||||
func TestTranslateError(t *testing.T) {
|
||||
event := AuditEvent{
|
||||
Type: EventTypeError,
|
||||
Message: "something went wrong",
|
||||
Error: errors.New("connection refused"),
|
||||
}
|
||||
|
||||
results := testSink.translateToPmgEvents(event)
|
||||
require.Len(t, results, 1)
|
||||
result := results[0]
|
||||
|
||||
assert.Equal(t, controltowerv1.PmgEventType_PMG_EVENT_TYPE_ERROR, result.GetEventType())
|
||||
require.True(t, result.HasError())
|
||||
|
||||
pmgErr := result.GetError()
|
||||
assert.Equal(t, "*errors.errorString", pmgErr.GetErrorType())
|
||||
assert.Equal(t, "something went wrong", pmgErr.GetMessage())
|
||||
}
|
||||
|
||||
func TestTranslateErrorNilError(t *testing.T) {
|
||||
event := AuditEvent{
|
||||
Type: EventTypeError,
|
||||
Message: "unknown issue",
|
||||
Error: nil,
|
||||
}
|
||||
|
||||
results := testSink.translateToPmgEvents(event)
|
||||
require.Len(t, results, 1)
|
||||
result := results[0]
|
||||
|
||||
pmgErr := result.GetError()
|
||||
assert.Equal(t, "", pmgErr.GetErrorType())
|
||||
assert.Equal(t, "unknown issue", pmgErr.GetMessage())
|
||||
}
|
||||
|
||||
func TestTranslateUnsupportedEventReturnsEmpty(t *testing.T) {
|
||||
unsupported := []EventType{
|
||||
EventTypeProxyHostObserved,
|
||||
EventTypeDependencyResolved,
|
||||
EventTypeInstallStarted,
|
||||
EventTypeInstallAllowed,
|
||||
EventTypeInstallTrustedAllowed,
|
||||
EventTypeInstallInsecureBypass,
|
||||
}
|
||||
|
||||
for _, et := range unsupported {
|
||||
t.Run(string(et), func(t *testing.T) {
|
||||
results := testSink.translateToPmgEvents(AuditEvent{Type: et})
|
||||
assert.Empty(t, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapPackageManager(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected controltowerv1.PmgPackageManager
|
||||
}{
|
||||
{"npm", "npm", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_NPM},
|
||||
{"npx", "npx", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_NPM},
|
||||
{"pnpm", "pnpm", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PNPM},
|
||||
{"pnpx", "pnpx", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PNPM},
|
||||
{"yarn", "yarn", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_YARN},
|
||||
{"bun", "bun", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_BUN},
|
||||
{"pip", "pip", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PIP},
|
||||
{"pip3", "pip3", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PIP},
|
||||
{"poetry", "poetry", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_POETRY},
|
||||
{"uv", "uv", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_UV},
|
||||
{"unknown", "cargo", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_UNSPECIFIED},
|
||||
{"empty", "", controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_UNSPECIFIED},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, mapPackageManager(tc.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateSessionComplete(t *testing.T) {
|
||||
event := AuditEvent{
|
||||
Type: EventTypeSessionComplete,
|
||||
SessionData: &SessionData{
|
||||
PackageManager: "npm",
|
||||
FlowType: FlowTypeProxy,
|
||||
Outcome: OutcomeSuccess,
|
||||
TotalAnalyzed: 10,
|
||||
AllowedCount: 8,
|
||||
BlockedCount: 1,
|
||||
ConfirmedCount: 1,
|
||||
TrustedSkipped: 2,
|
||||
InsecureBypassed: 0,
|
||||
Duration: 5 * time.Second,
|
||||
SandboxEnabled: true,
|
||||
ParanoidMode: false,
|
||||
TransitiveEnabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
results := testSink.translateToPmgEvents(event)
|
||||
require.Len(t, results, 1)
|
||||
|
||||
summary := results[0].GetSessionSummary()
|
||||
require.NotNil(t, summary)
|
||||
assert.Equal(t, controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_NPM, summary.GetPackageManager())
|
||||
assert.Equal(t, controltowerv1.PmgFlowType_PMG_FLOW_TYPE_PROXY, summary.GetFlowType())
|
||||
assert.Equal(t, uint32(10), summary.GetTotalAnalyzed())
|
||||
assert.Equal(t, uint32(8), summary.GetAllowedCount())
|
||||
assert.Equal(t, uint32(1), summary.GetBlockedCount())
|
||||
assert.Equal(t, uint32(1), summary.GetConfirmedCount())
|
||||
assert.Equal(t, uint32(2), summary.GetTrustedSkipped())
|
||||
assert.True(t, summary.GetSandboxEnabled())
|
||||
assert.False(t, summary.GetParanoidMode())
|
||||
assert.True(t, summary.GetTransitiveEnabled())
|
||||
assert.Equal(t, controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_SUCCESS, summary.GetOutcome())
|
||||
}
|
||||
|
||||
func TestTranslateSessionCompleteWithInsecureBypass(t *testing.T) {
|
||||
event := AuditEvent{
|
||||
Type: EventTypeSessionComplete,
|
||||
SessionData: &SessionData{
|
||||
PackageManager: "pip",
|
||||
FlowType: FlowTypeGuard,
|
||||
Outcome: OutcomeInsecureBypass,
|
||||
InsecureBypassed: 3,
|
||||
},
|
||||
}
|
||||
|
||||
results := testSink.translateToPmgEvents(event)
|
||||
require.Len(t, results, 2)
|
||||
|
||||
assert.Equal(t, controltowerv1.PmgEventType_PMG_EVENT_TYPE_SESSION_SUMMARY, results[0].GetEventType())
|
||||
|
||||
assert.Equal(t, controltowerv1.PmgEventType_PMG_EVENT_TYPE_INSECURE_BYPASS, results[1].GetEventType())
|
||||
bypass := results[1].GetInsecureBypass()
|
||||
require.NotNil(t, bypass)
|
||||
assert.Equal(t, controltowerv1.PmgPackageManager_PMG_PACKAGE_MANAGER_PIP, bypass.GetPackageManager())
|
||||
assert.Equal(t, uint32(3), bypass.GetPackagesBypassed())
|
||||
}
|
||||
|
||||
func TestTranslateSessionCompleteNilSessionData(t *testing.T) {
|
||||
event := AuditEvent{
|
||||
Type: EventTypeSessionComplete,
|
||||
SessionData: nil,
|
||||
}
|
||||
|
||||
results := testSink.translateToPmgEvents(event)
|
||||
assert.Empty(t, results)
|
||||
}
|
||||
|
||||
func TestMapFlowType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input FlowType
|
||||
expected controltowerv1.PmgFlowType
|
||||
}{
|
||||
{"guard", FlowTypeGuard, controltowerv1.PmgFlowType_PMG_FLOW_TYPE_GUARD},
|
||||
{"proxy", FlowTypeProxy, controltowerv1.PmgFlowType_PMG_FLOW_TYPE_PROXY},
|
||||
{"unknown", FlowType("other"), controltowerv1.PmgFlowType_PMG_FLOW_TYPE_UNSPECIFIED},
|
||||
{"empty", FlowType(""), controltowerv1.PmgFlowType_PMG_FLOW_TYPE_UNSPECIFIED},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, mapFlowType(tc.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapSessionOutcome(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input Outcome
|
||||
expected controltowerv1.PmgSessionOutcome
|
||||
}{
|
||||
{"success", OutcomeSuccess, controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_SUCCESS},
|
||||
{"blocked", OutcomeBlocked, controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_BLOCKED},
|
||||
{"user_cancelled", OutcomeUserCancelled, controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_USER_CANCELLED},
|
||||
{"error", OutcomeError, controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_ERROR},
|
||||
{"dry_run", OutcomeDryRun, controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_DRY_RUN},
|
||||
{"insecure_bypass", OutcomeInsecureBypass, controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_INSECURE_BYPASS},
|
||||
{"unknown", Outcome("other"), controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_UNSPECIFIED},
|
||||
{"empty", Outcome(""), controltowerv1.PmgSessionOutcome_PMG_SESSION_OUTCOME_UNSPECIFIED},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, mapSessionOutcome(tc.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,43 @@ import (
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
)
|
||||
|
||||
// SessionData carries aggregate session statistics for session-complete events.
|
||||
type SessionData struct {
|
||||
PackageManager string
|
||||
FlowType FlowType
|
||||
Outcome Outcome
|
||||
TotalAnalyzed uint32
|
||||
AllowedCount uint32
|
||||
BlockedCount uint32
|
||||
ConfirmedCount uint32
|
||||
TrustedSkipped uint32
|
||||
InsecureBypassed uint32
|
||||
Duration time.Duration
|
||||
SandboxEnabled bool
|
||||
ParanoidMode bool
|
||||
TransitiveEnabled bool
|
||||
}
|
||||
|
||||
// FlowType identifies how PMG intercepted the package installation.
|
||||
type FlowType string
|
||||
|
||||
const (
|
||||
FlowTypeGuard FlowType = "guard"
|
||||
FlowTypeProxy FlowType = "proxy"
|
||||
)
|
||||
|
||||
// Outcome describes the final result of a PMG invocation.
|
||||
type Outcome string
|
||||
|
||||
const (
|
||||
OutcomeSuccess Outcome = "success"
|
||||
OutcomeBlocked Outcome = "blocked"
|
||||
OutcomeUserCancelled Outcome = "user_cancelled"
|
||||
OutcomeDryRun Outcome = "dry_run"
|
||||
OutcomeError Outcome = "error"
|
||||
OutcomeInsecureBypass Outcome = "insecure_bypass"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
@@ -56,4 +93,7 @@ type AuditEvent struct {
|
||||
|
||||
// Error context
|
||||
Error error
|
||||
|
||||
// Session summary data — populated only for EventTypeSessionComplete
|
||||
SessionData *SessionData
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ func newEventlogSink() *eventlogSink {
|
||||
}
|
||||
|
||||
func (s *eventlogSink) Handle(_ context.Context, event AuditEvent) error {
|
||||
details := event.Details
|
||||
if details == nil && event.SessionData != nil {
|
||||
details = sessionDataToDetails(event.SessionData)
|
||||
}
|
||||
|
||||
elEvent := eventlog.Event{
|
||||
Timestamp: event.Timestamp,
|
||||
EventType: mapEventType(event.Type),
|
||||
@@ -20,11 +25,25 @@ func (s *eventlogSink) Handle(_ context.Context, event AuditEvent) error {
|
||||
PackageName: pkgName(event.PackageVersion),
|
||||
Version: pkgVersion(event.PackageVersion),
|
||||
Ecosystem: pkgEcosystem(event.PackageVersion),
|
||||
Details: event.Details,
|
||||
Details: details,
|
||||
}
|
||||
return eventlog.LogEvent(elEvent)
|
||||
}
|
||||
|
||||
func sessionDataToDetails(sd *SessionData) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"outcome": sd.Outcome,
|
||||
"flow_type": sd.FlowType,
|
||||
"package_manager": sd.PackageManager,
|
||||
"total_analyzed": sd.TotalAnalyzed,
|
||||
"allowed_count": sd.AllowedCount,
|
||||
"blocked_count": sd.BlockedCount,
|
||||
"confirmed_count": sd.ConfirmedCount,
|
||||
"trusted_skipped": sd.TrustedSkipped,
|
||||
"insecure_bypassed": sd.InsecureBypassed,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *eventlogSink) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/safedep/pmg/analyzer"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/guard"
|
||||
"github.com/safedep/pmg/internal/audit"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
"github.com/safedep/pmg/packagemanager"
|
||||
)
|
||||
@@ -110,6 +111,10 @@ func (f *commonFlow) Run(ctx context.Context, args []string, parsedCmd *packagem
|
||||
|
||||
reportData.Outcome = inferOutcome(cfg.InsecureInstallation, cfg.DryRun, blockedCount, userCancelledCount, err)
|
||||
|
||||
// Session complete is called here (not deferred) because guard.Run() calls
|
||||
// LogInstallStarted internally, and all paths after guard.Run() reach this point.
|
||||
audit.LogSessionComplete(audit.Outcome(reportData.Outcome.String()), audit.FlowTypeGuard)
|
||||
|
||||
// Show the report
|
||||
ui.Report(reportData)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/safedep/pmg/analyzer"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/guard"
|
||||
"github.com/safedep/pmg/internal/audit"
|
||||
"github.com/safedep/pmg/internal/pty"
|
||||
"github.com/safedep/pmg/internal/ui"
|
||||
"github.com/safedep/pmg/packagemanager"
|
||||
@@ -38,7 +39,7 @@ func ProxyFlow(pm packagemanager.PackageManager, packageResolver packagemanager.
|
||||
}
|
||||
|
||||
// Run executes the proxy-based flow
|
||||
func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagemanager.ParsedCommand) error {
|
||||
func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagemanager.ParsedCommand) (runErr error) {
|
||||
// Check if we have a supported ecosystem else fail fast
|
||||
ecosystem := f.pm.Ecosystem()
|
||||
if !interceptors.IsSupported(ecosystem) {
|
||||
@@ -72,6 +73,18 @@ func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagema
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
audit.LogInstallStarted(f.pm.Name(), args)
|
||||
defer func() {
|
||||
// On early error returns (e.g. CA cert, analyzer init), reportData.Outcome
|
||||
// is still the default (Success). Override to Error for these cases. Don't
|
||||
// override when outcome was explicitly set (e.g. Blocked) — those paths may
|
||||
// also return an error from the underlying package manager command.
|
||||
if runErr != nil && reportData.Outcome == ui.OutcomeSuccess {
|
||||
reportData.Outcome = ui.OutcomeError
|
||||
}
|
||||
audit.LogSessionComplete(audit.Outcome(reportData.Outcome.String()), audit.FlowTypeProxy)
|
||||
}()
|
||||
|
||||
// Check if dry-run mode is enabled
|
||||
if cfg.DryRun {
|
||||
log.Infof("Dry-run mode: Would execute %s with experimental proxy protection", f.pm.Name())
|
||||
|
||||
Reference in New Issue
Block a user