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
+20 -4
View File
@@ -1,6 +1,11 @@
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
// 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
// (remaining == 0), it reports the oldest version (closest to exiting cooldown).
// 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 {
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 {
oldestVer, oldestDate := cooldownOldestVersion(dates)
if oldestVer != "" {
_, daysAgo, daysLeft := cooldownIsWithinWindow(oldestDate, cooldownDays)
statsCollector.RecordCooldownBlocked(packageName, oldestVer, oldestDate, daysAgo, daysLeft, cooldownDays)
logCooldown(oldestVer, oldestDate, daysAgo, daysLeft)
}
} else if pinnedVersion != "" {
if pinnedDate, ok := dates[pinnedVersion]; ok {
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"
"time"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/log"
"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)",
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,
// npm would serve the stripped metadata from cache even after the
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"time"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/log"
"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)",
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")
return statusCode, headers, strippedBody, nil