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:
Sahil Bansal
2026-05-08 10:01:25 +05:30
committed by GitHub
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
12 changed files with 218 additions and 67 deletions
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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++
}
+37
View File
@@ -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)
+64 -14
View File
@@ -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
View File
@@ -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
+12 -9
View File
@@ -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:
+1
View File
@@ -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"
) )
+20 -4
View File
@@ -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)
} }
} }
} }
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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