mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: emit cloud events for dependency cooldown and host observations (#243)
* feat: emit cloud events for dependency cooldown and host observations (#237) Wire cooldown blocks and proxy host observations through the cloud sync pipeline so they appear as telemetry in Control Tower. - Cooldown blocks emit PACKAGE_DECISION with COOLDOWN_BLOCKED action and PmgDependencyCooldown context (publish date, cooldown days, days since publish, days remaining) - Proxy host observations emit HOST_OBSERVATION with PmgHostObservation (hostname, method) - Session summary now includes cooldown_blocked_count - Updated buf API dependency for new proto schema Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * format file * fix: add explicit eventlog mapping for EventTypeDependencyCooldown Follow the existing pattern where every audit event type has an explicit case in mapEventType and a corresponding constant in the eventlog package. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Apply suggestion from @devin-ai-integration[bot] 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> --------- Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-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
Claude Opus 4.6
devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Abhisek Datta
devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
b671192598
commit
19d735cdd2
@@ -3,8 +3,8 @@ module github.com/safedep/pmg
|
|||||||
go 1.25.1
|
go 1.25.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260409081445-73994c4e35a3.1
|
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260507092425-ac47f9a19339.1
|
||||||
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260409081445-73994c4e35a3.1
|
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260507092425-ac47f9a19339.1
|
||||||
github.com/Masterminds/semver v1.5.0
|
github.com/Masterminds/semver v1.5.0
|
||||||
github.com/elazarl/goproxy v1.8.1
|
github.com/elazarl/goproxy v1.8.1
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-202405082006
|
|||||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20240508200655-46a4cf4ba109.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20240508200655-46a4cf4ba109.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
|
||||||
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260409081445-73994c4e35a3.1 h1:OKKSMXf1k7NZ1qcp5gfpTw0F3QGVM/X6ghkRs9GtrIA=
|
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260409081445-73994c4e35a3.1 h1:OKKSMXf1k7NZ1qcp5gfpTw0F3QGVM/X6ghkRs9GtrIA=
|
||||||
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260409081445-73994c4e35a3.1/go.mod h1:k1tkPvr2SWI5hWfY/fF9XgIBh1sZOMPMPl/bUH3L0wE=
|
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260409081445-73994c4e35a3.1/go.mod h1:k1tkPvr2SWI5hWfY/fF9XgIBh1sZOMPMPl/bUH3L0wE=
|
||||||
|
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260507092425-ac47f9a19339.1 h1:TgL0Xu+EFQhr68C/eGrflL+TaFtEUWkfWXQ+f+vWGdg=
|
||||||
|
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260507092425-ac47f9a19339.1/go.mod h1:AU7tshd3hSyemWEYn6mofmWPUJaWmGExFHAQSjp6N4o=
|
||||||
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260409081445-73994c4e35a3.1 h1:x5a6h/YeT2cqgxqmBGOCKlifMEMb4VRJ3eOgQkNvJrs=
|
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260409081445-73994c4e35a3.1 h1:x5a6h/YeT2cqgxqmBGOCKlifMEMb4VRJ3eOgQkNvJrs=
|
||||||
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260409081445-73994c4e35a3.1/go.mod h1:I8E+sZXJNqzWBtSlRGCoiEorLSRiix50h2R/66aBzME=
|
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260409081445-73994c4e35a3.1/go.mod h1:I8E+sZXJNqzWBtSlRGCoiEorLSRiix50h2R/66aBzME=
|
||||||
|
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260507092425-ac47f9a19339.1 h1:JRzHMhoJg1Mlae+PR+ZZ1I1aaqAJdyF4WY3CidJD/us=
|
||||||
|
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260507092425-ac47f9a19339.1/go.mod h1:I8E+sZXJNqzWBtSlRGCoiEorLSRiix50h2R/66aBzME=
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
|
|||||||
+31
-13
@@ -199,6 +199,23 @@ func LogProxyHostObserved(hostname, method, reason string, details map[string]in
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogDependencyCooldown records that a package was blocked by the dependency cooldown policy.
|
||||||
|
func LogDependencyCooldown(pv *packagev1.PackageVersion, publishDate time.Time, cooldownDays, daysAgo, daysLeft int) {
|
||||||
|
logEvent(AuditEvent{
|
||||||
|
Type: EventTypeDependencyCooldown,
|
||||||
|
Message: fmt.Sprintf("Package blocked by cooldown policy: %s@%s (published %d days ago, %d days remaining)", pkgName(pv), pkgVersion(pv), daysAgo, daysLeft),
|
||||||
|
PackageVersion: pv,
|
||||||
|
PublishDate: publishDate,
|
||||||
|
CooldownDays: cooldownDays,
|
||||||
|
DaysAgo: daysAgo,
|
||||||
|
DaysLeft: daysLeft,
|
||||||
|
})
|
||||||
|
|
||||||
|
if global != nil {
|
||||||
|
global.recordCooldownBlocked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LogSandboxOverride records that runtime sandbox policy overrides were applied.
|
// LogSandboxOverride records that runtime sandbox policy overrides were applied.
|
||||||
func LogSandboxOverride(sandboxProfile string, overrides []map[string]string) {
|
func LogSandboxOverride(sandboxProfile string, overrides []map[string]string) {
|
||||||
logEvent(AuditEvent{
|
logEvent(AuditEvent{
|
||||||
@@ -250,19 +267,20 @@ func LogSessionComplete(outcome Outcome, flowType FlowType) {
|
|||||||
Type: EventTypeSessionComplete,
|
Type: EventTypeSessionComplete,
|
||||||
Message: fmt.Sprintf("Session complete: %s", outcome),
|
Message: fmt.Sprintf("Session complete: %s", outcome),
|
||||||
SessionData: &SessionData{
|
SessionData: &SessionData{
|
||||||
PackageManager: s.packageManager,
|
PackageManager: s.packageManager,
|
||||||
FlowType: flowType,
|
FlowType: flowType,
|
||||||
Outcome: outcome,
|
Outcome: outcome,
|
||||||
TotalAnalyzed: s.totalAnalyzed,
|
TotalAnalyzed: s.totalAnalyzed,
|
||||||
AllowedCount: s.allowedCount,
|
AllowedCount: s.allowedCount,
|
||||||
BlockedCount: s.blockedCount,
|
BlockedCount: s.blockedCount,
|
||||||
ConfirmedCount: s.confirmedCount,
|
ConfirmedCount: s.confirmedCount,
|
||||||
TrustedSkipped: s.trustedSkipped,
|
TrustedSkipped: s.trustedSkipped,
|
||||||
InsecureBypassed: s.insecureBypassed,
|
InsecureBypassed: s.insecureBypassed,
|
||||||
Duration: time.Since(s.startTime),
|
CooldownBlockedCount: s.cooldownBlockedCount,
|
||||||
SandboxEnabled: cfg.Config.Sandbox.Enabled,
|
Duration: time.Since(s.startTime),
|
||||||
ParanoidMode: cfg.Config.Paranoid,
|
SandboxEnabled: cfg.Config.Sandbox.Enabled,
|
||||||
TransitiveEnabled: cfg.Config.Transitive,
|
ParanoidMode: cfg.Config.Paranoid,
|
||||||
|
TransitiveEnabled: cfg.Config.Transitive,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-10
@@ -9,16 +9,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type session struct {
|
type session struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
packageManager string
|
packageManager string
|
||||||
args []string
|
args []string
|
||||||
totalAnalyzed uint32
|
totalAnalyzed uint32
|
||||||
allowedCount uint32
|
allowedCount uint32
|
||||||
blockedCount uint32
|
blockedCount uint32
|
||||||
confirmedCount uint32
|
confirmedCount uint32
|
||||||
trustedSkipped uint32
|
trustedSkipped uint32
|
||||||
insecureBypassed uint32
|
insecureBypassed uint32
|
||||||
|
cooldownBlockedCount uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type auditor struct {
|
type auditor struct {
|
||||||
@@ -125,3 +126,14 @@ func (a *auditor) recordInsecureBypassed() {
|
|||||||
s.insecureBypassed++
|
s.insecureBypassed++
|
||||||
s.totalAnalyzed++
|
s.totalAnalyzed++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *auditor) recordCooldownBlocked() {
|
||||||
|
s := a.getSession()
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.cooldownBlockedCount++
|
||||||
|
s.totalAnalyzed++
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
|
controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
|
||||||
"google.golang.org/protobuf/types/known/durationpb"
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *cloudSink) translateToPmgEvents(event AuditEvent) []*controltowerv1.PmgEvent {
|
func (s *cloudSink) translateToPmgEvents(event AuditEvent) []*controltowerv1.PmgEvent {
|
||||||
@@ -18,6 +19,10 @@ func (s *cloudSink) translateToPmgEvents(event AuditEvent) []*controltowerv1.Pmg
|
|||||||
// not a per-package event. It is emitted as part of EventTypeSessionComplete when
|
// not a per-package event. It is emitted as part of EventTypeSessionComplete when
|
||||||
// the session's insecureBypassed counter is > 0.
|
// the session's insecureBypassed counter is > 0.
|
||||||
return nil
|
return nil
|
||||||
|
case EventTypeDependencyCooldown:
|
||||||
|
return []*controltowerv1.PmgEvent{newCooldownBlockedEvent(event)}
|
||||||
|
case EventTypeProxyHostObserved:
|
||||||
|
return []*controltowerv1.PmgEvent{newHostObservationEvent(event)}
|
||||||
case EventTypeSandboxOverride:
|
case EventTypeSandboxOverride:
|
||||||
return []*controltowerv1.PmgEvent{newSandboxOverrideEvent(event)}
|
return []*controltowerv1.PmgEvent{newSandboxOverrideEvent(event)}
|
||||||
case EventTypeError:
|
case EventTypeError:
|
||||||
@@ -84,6 +89,37 @@ func newErrorEvent(event AuditEvent) *controltowerv1.PmgEvent {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newCooldownBlockedEvent(event AuditEvent) *controltowerv1.PmgEvent {
|
||||||
|
decision := &controltowerv1.PmgPackageDecision{}
|
||||||
|
decision.SetPackageVersion(event.PackageVersion)
|
||||||
|
decision.SetAction(controltowerv1.PmgPackageAction_PMG_PACKAGE_ACTION_COOLDOWN_BLOCKED)
|
||||||
|
|
||||||
|
cooldown := &controltowerv1.PmgDependencyCooldown{}
|
||||||
|
if !event.PublishDate.IsZero() {
|
||||||
|
cooldown.SetPublishDate(timestamppb.New(event.PublishDate))
|
||||||
|
}
|
||||||
|
cooldown.SetCooldownDays(uint32(event.CooldownDays))
|
||||||
|
cooldown.SetDaysSincePublish(uint32(event.DaysAgo))
|
||||||
|
cooldown.SetDaysRemaining(uint32(event.DaysLeft))
|
||||||
|
decision.SetCooldown(cooldown)
|
||||||
|
|
||||||
|
e := &controltowerv1.PmgEvent{}
|
||||||
|
e.SetEventType(controltowerv1.PmgEventType_PMG_EVENT_TYPE_PACKAGE_DECISION)
|
||||||
|
e.SetPackageDecision(decision)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHostObservationEvent(event AuditEvent) *controltowerv1.PmgEvent {
|
||||||
|
obs := &controltowerv1.PmgHostObservation{}
|
||||||
|
obs.SetHostname(event.Hostname)
|
||||||
|
obs.SetMethod(event.Method)
|
||||||
|
|
||||||
|
e := &controltowerv1.PmgEvent{}
|
||||||
|
e.SetEventType(controltowerv1.PmgEventType_PMG_EVENT_TYPE_HOST_OBSERVATION)
|
||||||
|
e.SetHostObservation(obs)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
func newSessionSummaryEvent(data *SessionData) *controltowerv1.PmgEvent {
|
func newSessionSummaryEvent(data *SessionData) *controltowerv1.PmgEvent {
|
||||||
summary := &controltowerv1.PmgSessionSummary{}
|
summary := &controltowerv1.PmgSessionSummary{}
|
||||||
summary.SetPackageManager(mapPackageManager(data.PackageManager))
|
summary.SetPackageManager(mapPackageManager(data.PackageManager))
|
||||||
@@ -93,6 +129,7 @@ func newSessionSummaryEvent(data *SessionData) *controltowerv1.PmgEvent {
|
|||||||
summary.SetBlockedCount(data.BlockedCount)
|
summary.SetBlockedCount(data.BlockedCount)
|
||||||
summary.SetConfirmedCount(data.ConfirmedCount)
|
summary.SetConfirmedCount(data.ConfirmedCount)
|
||||||
summary.SetTrustedSkipped(data.TrustedSkipped)
|
summary.SetTrustedSkipped(data.TrustedSkipped)
|
||||||
|
summary.SetCooldownBlockedCount(data.CooldownBlockedCount)
|
||||||
summary.SetDuration(durationpb.New(data.Duration))
|
summary.SetDuration(durationpb.New(data.Duration))
|
||||||
summary.SetSandboxEnabled(data.SandboxEnabled)
|
summary.SetSandboxEnabled(data.SandboxEnabled)
|
||||||
summary.SetParanoidMode(data.ParanoidMode)
|
summary.SetParanoidMode(data.ParanoidMode)
|
||||||
|
|||||||
@@ -127,9 +127,57 @@ func TestTranslateErrorNilError(t *testing.T) {
|
|||||||
assert.Equal(t, "unknown issue", pmgErr.GetMessage())
|
assert.Equal(t, "unknown issue", pmgErr.GetMessage())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTranslateCooldownBlocked(t *testing.T) {
|
||||||
|
publishDate := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
event := AuditEvent{
|
||||||
|
Type: EventTypeDependencyCooldown,
|
||||||
|
PackageVersion: testPackageVersion("new-pkg", "1.0.0", "npm"),
|
||||||
|
PublishDate: publishDate,
|
||||||
|
CooldownDays: 30,
|
||||||
|
DaysAgo: 6,
|
||||||
|
DaysLeft: 24,
|
||||||
|
}
|
||||||
|
|
||||||
|
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_COOLDOWN_BLOCKED, decision.GetAction())
|
||||||
|
assert.NotNil(t, decision.GetPackageVersion())
|
||||||
|
|
||||||
|
require.True(t, decision.HasCooldown())
|
||||||
|
cooldown := decision.GetCooldown()
|
||||||
|
assert.Equal(t, publishDate.Unix(), cooldown.GetPublishDate().AsTime().Unix())
|
||||||
|
assert.Equal(t, uint32(30), cooldown.GetCooldownDays())
|
||||||
|
assert.Equal(t, uint32(6), cooldown.GetDaysSincePublish())
|
||||||
|
assert.Equal(t, uint32(24), cooldown.GetDaysRemaining())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTranslateHostObservation(t *testing.T) {
|
||||||
|
event := AuditEvent{
|
||||||
|
Type: EventTypeProxyHostObserved,
|
||||||
|
Hostname: "evil.example.com",
|
||||||
|
Method: "CONNECT",
|
||||||
|
}
|
||||||
|
|
||||||
|
results := testSink.translateToPmgEvents(event)
|
||||||
|
require.Len(t, results, 1)
|
||||||
|
result := results[0]
|
||||||
|
|
||||||
|
assert.Equal(t, controltowerv1.PmgEventType_PMG_EVENT_TYPE_HOST_OBSERVATION, result.GetEventType())
|
||||||
|
require.True(t, result.HasHostObservation())
|
||||||
|
|
||||||
|
obs := result.GetHostObservation()
|
||||||
|
assert.Equal(t, "evil.example.com", obs.GetHostname())
|
||||||
|
assert.Equal(t, "CONNECT", obs.GetMethod())
|
||||||
|
}
|
||||||
|
|
||||||
func TestTranslateUnsupportedEventReturnsEmpty(t *testing.T) {
|
func TestTranslateUnsupportedEventReturnsEmpty(t *testing.T) {
|
||||||
unsupported := []EventType{
|
unsupported := []EventType{
|
||||||
EventTypeProxyHostObserved,
|
|
||||||
EventTypeDependencyResolved,
|
EventTypeDependencyResolved,
|
||||||
EventTypeInstallStarted,
|
EventTypeInstallStarted,
|
||||||
EventTypeInstallAllowed,
|
EventTypeInstallAllowed,
|
||||||
@@ -176,19 +224,20 @@ func TestTranslateSessionComplete(t *testing.T) {
|
|||||||
event := AuditEvent{
|
event := AuditEvent{
|
||||||
Type: EventTypeSessionComplete,
|
Type: EventTypeSessionComplete,
|
||||||
SessionData: &SessionData{
|
SessionData: &SessionData{
|
||||||
PackageManager: "npm",
|
PackageManager: "npm",
|
||||||
FlowType: FlowTypeProxy,
|
FlowType: FlowTypeProxy,
|
||||||
Outcome: OutcomeSuccess,
|
Outcome: OutcomeSuccess,
|
||||||
TotalAnalyzed: 10,
|
TotalAnalyzed: 10,
|
||||||
AllowedCount: 8,
|
AllowedCount: 8,
|
||||||
BlockedCount: 1,
|
BlockedCount: 1,
|
||||||
ConfirmedCount: 1,
|
ConfirmedCount: 1,
|
||||||
TrustedSkipped: 2,
|
TrustedSkipped: 2,
|
||||||
InsecureBypassed: 0,
|
InsecureBypassed: 0,
|
||||||
Duration: 5 * time.Second,
|
CooldownBlockedCount: 3,
|
||||||
SandboxEnabled: true,
|
Duration: 5 * time.Second,
|
||||||
ParanoidMode: false,
|
SandboxEnabled: true,
|
||||||
TransitiveEnabled: true,
|
ParanoidMode: false,
|
||||||
|
TransitiveEnabled: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +253,7 @@ func TestTranslateSessionComplete(t *testing.T) {
|
|||||||
assert.Equal(t, uint32(1), summary.GetBlockedCount())
|
assert.Equal(t, uint32(1), summary.GetBlockedCount())
|
||||||
assert.Equal(t, uint32(1), summary.GetConfirmedCount())
|
assert.Equal(t, uint32(1), summary.GetConfirmedCount())
|
||||||
assert.Equal(t, uint32(2), summary.GetTrustedSkipped())
|
assert.Equal(t, uint32(2), summary.GetTrustedSkipped())
|
||||||
|
assert.Equal(t, uint32(3), summary.GetCooldownBlockedCount())
|
||||||
assert.True(t, summary.GetSandboxEnabled())
|
assert.True(t, summary.GetSandboxEnabled())
|
||||||
assert.False(t, summary.GetParanoidMode())
|
assert.False(t, summary.GetParanoidMode())
|
||||||
assert.True(t, summary.GetTransitiveEnabled())
|
assert.True(t, summary.GetTransitiveEnabled())
|
||||||
|
|||||||
+21
-13
@@ -8,19 +8,20 @@ import (
|
|||||||
|
|
||||||
// SessionData carries aggregate session statistics for session-complete events.
|
// SessionData carries aggregate session statistics for session-complete events.
|
||||||
type SessionData struct {
|
type SessionData struct {
|
||||||
PackageManager string
|
PackageManager string
|
||||||
FlowType FlowType
|
FlowType FlowType
|
||||||
Outcome Outcome
|
Outcome Outcome
|
||||||
TotalAnalyzed uint32
|
TotalAnalyzed uint32
|
||||||
AllowedCount uint32
|
AllowedCount uint32
|
||||||
BlockedCount uint32
|
BlockedCount uint32
|
||||||
ConfirmedCount uint32
|
ConfirmedCount uint32
|
||||||
TrustedSkipped uint32
|
TrustedSkipped uint32
|
||||||
InsecureBypassed uint32
|
InsecureBypassed uint32
|
||||||
Duration time.Duration
|
CooldownBlockedCount uint32
|
||||||
SandboxEnabled bool
|
Duration time.Duration
|
||||||
ParanoidMode bool
|
SandboxEnabled bool
|
||||||
TransitiveEnabled bool
|
ParanoidMode bool
|
||||||
|
TransitiveEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlowType identifies how PMG intercepted the package installation.
|
// FlowType identifies how PMG intercepted the package installation.
|
||||||
@@ -54,6 +55,7 @@ const (
|
|||||||
EventTypeDependencyResolved EventType = "dependency_resolved"
|
EventTypeDependencyResolved EventType = "dependency_resolved"
|
||||||
EventTypeInstallInsecureBypass EventType = "install_insecure_bypass"
|
EventTypeInstallInsecureBypass EventType = "install_insecure_bypass"
|
||||||
EventTypeProxyHostObserved EventType = "proxy_host_observed"
|
EventTypeProxyHostObserved EventType = "proxy_host_observed"
|
||||||
|
EventTypeDependencyCooldown EventType = "dependency_cooldown"
|
||||||
EventTypeSandboxOverride EventType = "sandbox_override"
|
EventTypeSandboxOverride EventType = "sandbox_override"
|
||||||
EventTypeError EventType = "error"
|
EventTypeError EventType = "error"
|
||||||
EventTypeSessionComplete EventType = "session_complete"
|
EventTypeSessionComplete EventType = "session_complete"
|
||||||
@@ -91,6 +93,12 @@ type AuditEvent struct {
|
|||||||
Method string
|
Method string
|
||||||
Reason string
|
Reason string
|
||||||
|
|
||||||
|
// Cooldown context
|
||||||
|
PublishDate time.Time
|
||||||
|
CooldownDays int
|
||||||
|
DaysAgo int
|
||||||
|
DaysLeft int
|
||||||
|
|
||||||
// Error context
|
// Error context
|
||||||
Error error
|
Error error
|
||||||
|
|
||||||
|
|||||||
@@ -32,15 +32,16 @@ func (s *eventlogSink) Handle(_ context.Context, event AuditEvent) error {
|
|||||||
|
|
||||||
func sessionDataToDetails(sd *SessionData) map[string]interface{} {
|
func sessionDataToDetails(sd *SessionData) map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"outcome": sd.Outcome,
|
"outcome": sd.Outcome,
|
||||||
"flow_type": sd.FlowType,
|
"flow_type": sd.FlowType,
|
||||||
"package_manager": sd.PackageManager,
|
"package_manager": sd.PackageManager,
|
||||||
"total_analyzed": sd.TotalAnalyzed,
|
"total_analyzed": sd.TotalAnalyzed,
|
||||||
"allowed_count": sd.AllowedCount,
|
"allowed_count": sd.AllowedCount,
|
||||||
"blocked_count": sd.BlockedCount,
|
"blocked_count": sd.BlockedCount,
|
||||||
"confirmed_count": sd.ConfirmedCount,
|
"confirmed_count": sd.ConfirmedCount,
|
||||||
"trusted_skipped": sd.TrustedSkipped,
|
"trusted_skipped": sd.TrustedSkipped,
|
||||||
"insecure_bypassed": sd.InsecureBypassed,
|
"insecure_bypassed": sd.InsecureBypassed,
|
||||||
|
"cooldown_blocked_count": sd.CooldownBlockedCount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +67,8 @@ func mapEventType(t EventType) eventlog.EventType {
|
|||||||
return eventlog.EventTypeInstallInsecureBypass
|
return eventlog.EventTypeInstallInsecureBypass
|
||||||
case EventTypeProxyHostObserved:
|
case EventTypeProxyHostObserved:
|
||||||
return eventlog.EventTypeProxyHostObserved
|
return eventlog.EventTypeProxyHostObserved
|
||||||
|
case EventTypeDependencyCooldown:
|
||||||
|
return eventlog.EventTypeDependencyCooldown
|
||||||
case EventTypeSandboxOverride:
|
case EventTypeSandboxOverride:
|
||||||
return eventlog.EventTypeSandboxOverride
|
return eventlog.EventTypeSandboxOverride
|
||||||
case EventTypeError:
|
case EventTypeError:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const (
|
|||||||
EventTypeDependencyResolved EventType = "dependency_resolved"
|
EventTypeDependencyResolved EventType = "dependency_resolved"
|
||||||
EventTypeInstallInsecureBypass EventType = "install_insecure_bypass"
|
EventTypeInstallInsecureBypass EventType = "install_insecure_bypass"
|
||||||
EventTypeProxyHostObserved EventType = "proxy_host_observed"
|
EventTypeProxyHostObserved EventType = "proxy_host_observed"
|
||||||
|
EventTypeDependencyCooldown EventType = "dependency_cooldown"
|
||||||
EventTypeSandboxOverride EventType = "sandbox_override"
|
EventTypeSandboxOverride EventType = "sandbox_override"
|
||||||
EventTypeError EventType = "error"
|
EventTypeError EventType = "error"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package interceptors
|
package interceptors
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||||
|
"github.com/safedep/pmg/internal/audit"
|
||||||
|
)
|
||||||
|
|
||||||
// cooldownIsWithinWindow reports whether a version published at publishDate is still
|
// cooldownIsWithinWindow reports whether a version published at publishDate is still
|
||||||
// within the cooldown window of cooldownDays. Returns withinCooldown, daysSincePublish,
|
// within the cooldown window of cooldownDays. Returns withinCooldown, daysSincePublish,
|
||||||
@@ -34,21 +39,32 @@ func cooldownOldestVersion(dates map[string]time.Time) (string, time.Time) {
|
|||||||
// recordCooldownStats records a cooldown block event. When all versions are blocked
|
// recordCooldownStats records a cooldown block event. When all versions are blocked
|
||||||
// (remaining == 0), it reports the oldest version (closest to exiting cooldown).
|
// (remaining == 0), it reports the oldest version (closest to exiting cooldown).
|
||||||
// Otherwise, if a pinned version was stripped, it reports that specific version.
|
// Otherwise, if a pinned version was stripped, it reports that specific version.
|
||||||
func recordCooldownStats(statsCollector *AnalysisStatsCollector, packageName string, pinnedVersion string, dates map[string]time.Time, remaining int, cooldownDays int) {
|
func recordCooldownStats(statsCollector *AnalysisStatsCollector, ecosystem packagev1.Ecosystem, packageName string, pinnedVersion string, dates map[string]time.Time, remaining int, cooldownDays int) {
|
||||||
if statsCollector == nil {
|
if statsCollector == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logCooldown := func(version string, publishDate time.Time, daysAgo, daysLeft int) {
|
||||||
|
statsCollector.RecordCooldownBlocked(packageName, version, publishDate, daysAgo, daysLeft, cooldownDays)
|
||||||
|
|
||||||
|
pv := &packagev1.PackageVersion{}
|
||||||
|
pv.SetPackage(&packagev1.Package{})
|
||||||
|
pv.GetPackage().SetName(packageName)
|
||||||
|
pv.GetPackage().SetEcosystem(ecosystem)
|
||||||
|
pv.SetVersion(version)
|
||||||
|
audit.LogDependencyCooldown(pv, publishDate, cooldownDays, daysAgo, daysLeft)
|
||||||
|
}
|
||||||
|
|
||||||
if remaining == 0 {
|
if remaining == 0 {
|
||||||
oldestVer, oldestDate := cooldownOldestVersion(dates)
|
oldestVer, oldestDate := cooldownOldestVersion(dates)
|
||||||
if oldestVer != "" {
|
if oldestVer != "" {
|
||||||
_, daysAgo, daysLeft := cooldownIsWithinWindow(oldestDate, cooldownDays)
|
_, daysAgo, daysLeft := cooldownIsWithinWindow(oldestDate, cooldownDays)
|
||||||
statsCollector.RecordCooldownBlocked(packageName, oldestVer, oldestDate, daysAgo, daysLeft, cooldownDays)
|
logCooldown(oldestVer, oldestDate, daysAgo, daysLeft)
|
||||||
}
|
}
|
||||||
} else if pinnedVersion != "" {
|
} else if pinnedVersion != "" {
|
||||||
if pinnedDate, ok := dates[pinnedVersion]; ok {
|
if pinnedDate, ok := dates[pinnedVersion]; ok {
|
||||||
if withinCooldown, daysAgo, daysLeft := cooldownIsWithinWindow(pinnedDate, cooldownDays); withinCooldown {
|
if withinCooldown, daysAgo, daysLeft := cooldownIsWithinWindow(pinnedDate, cooldownDays); withinCooldown {
|
||||||
statsCollector.RecordCooldownBlocked(packageName, pinnedVersion, pinnedDate, daysAgo, daysLeft, cooldownDays)
|
logCooldown(pinnedVersion, pinnedDate, daysAgo, daysLeft)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||||
"github.com/safedep/dry/log"
|
"github.com/safedep/dry/log"
|
||||||
"github.com/safedep/pmg/proxy"
|
"github.com/safedep/pmg/proxy"
|
||||||
)
|
)
|
||||||
@@ -66,7 +67,7 @@ func (h *npmCooldownHandler) HandleMetadataRequest(ctx *proxy.RequestContext, pa
|
|||||||
log.Infof("[%s] Cooldown: stripped %d version(s) from %s metadata (%d days, %d eligible remain)",
|
log.Infof("[%s] Cooldown: stripped %d version(s) from %s metadata (%d days, %d eligible remain)",
|
||||||
ctx.RequestID, stripped, packageName, cooldownDays, remaining)
|
ctx.RequestID, stripped, packageName, cooldownDays, remaining)
|
||||||
|
|
||||||
recordCooldownStats(h.statsCollector, packageName, pinnedVersion, dates, remaining, cooldownDays)
|
recordCooldownStats(h.statsCollector, packagev1.Ecosystem_ECOSYSTEM_NPM, packageName, pinnedVersion, dates, remaining, cooldownDays)
|
||||||
|
|
||||||
// Prevent npm from caching the modified response. Without this,
|
// Prevent npm from caching the modified response. Without this,
|
||||||
// npm would serve the stripped metadata from cache even after the
|
// npm would serve the stripped metadata from cache even after the
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||||
"github.com/safedep/dry/log"
|
"github.com/safedep/dry/log"
|
||||||
"github.com/safedep/pmg/proxy"
|
"github.com/safedep/pmg/proxy"
|
||||||
)
|
)
|
||||||
@@ -53,7 +54,7 @@ func (h *pypiCooldownHandler) HandleMetadataRequest(ctx *proxy.RequestContext, p
|
|||||||
log.Infof("[%s] Cooldown: stripped %d version(s) from %s metadata (%d days, %d eligible remain)",
|
log.Infof("[%s] Cooldown: stripped %d version(s) from %s metadata (%d days, %d eligible remain)",
|
||||||
ctx.RequestID, stripped, packageName, cooldownDays, remaining)
|
ctx.RequestID, stripped, packageName, cooldownDays, remaining)
|
||||||
|
|
||||||
recordCooldownStats(h.statsCollector, packageName, pinnedVersion, dates, remaining, cooldownDays)
|
recordCooldownStats(h.statsCollector, packagev1.Ecosystem_ECOSYSTEM_PYPI, packageName, pinnedVersion, dates, remaining, cooldownDays)
|
||||||
|
|
||||||
headers.Set("Cache-Control", "no-store")
|
headers.Set("Cache-Control", "no-store")
|
||||||
return statusCode, headers, strippedBody, nil
|
return statusCode, headers, strippedBody, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user