2026-03-04 19:00:31 +00:00
|
|
|
package cooldown
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-18 10:59:29 +00:00
|
|
|
const hoursPerDay = 24
|
|
|
|
|
|
2026-03-04 19:00:31 +00:00
|
|
|
// Config holds cooldown settings for version filtering.
|
|
|
|
|
// Cooldown hides package versions published too recently, giving the community
|
|
|
|
|
// time to spot malicious releases before they're pulled into projects.
|
|
|
|
|
type Config struct {
|
|
|
|
|
// Default is the global default cooldown duration (e.g., "3d", "48h").
|
|
|
|
|
Default string `json:"default" yaml:"default"`
|
|
|
|
|
|
|
|
|
|
// Ecosystems overrides the default for specific ecosystems.
|
|
|
|
|
// Keys are ecosystem names (e.g., "npm", "pypi").
|
|
|
|
|
Ecosystems map[string]string `json:"ecosystems" yaml:"ecosystems"`
|
|
|
|
|
|
|
|
|
|
// Packages overrides the cooldown for specific packages.
|
|
|
|
|
// Keys are PURLs (e.g., "pkg:npm/lodash", "pkg:npm/@babel/core").
|
|
|
|
|
Packages map[string]string `json:"packages" yaml:"packages"`
|
|
|
|
|
|
|
|
|
|
defaultDuration time.Duration
|
|
|
|
|
ecosystemDurations map[string]time.Duration
|
|
|
|
|
packageDurations map[string]time.Duration
|
|
|
|
|
parsed bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parse resolves all string durations into time.Duration values.
|
|
|
|
|
// Called lazily on first use.
|
|
|
|
|
func (c *Config) parse() {
|
|
|
|
|
if c.parsed {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.parsed = true
|
|
|
|
|
|
|
|
|
|
c.defaultDuration, _ = ParseDuration(c.Default)
|
|
|
|
|
|
|
|
|
|
c.ecosystemDurations = make(map[string]time.Duration, len(c.Ecosystems))
|
|
|
|
|
for k, v := range c.Ecosystems {
|
|
|
|
|
d, _ := ParseDuration(v)
|
|
|
|
|
c.ecosystemDurations[k] = d
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.packageDurations = make(map[string]time.Duration, len(c.Packages))
|
|
|
|
|
for k, v := range c.Packages {
|
|
|
|
|
d, _ := ParseDuration(v)
|
|
|
|
|
c.packageDurations[k] = d
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For returns the effective cooldown duration for a given ecosystem and package PURL.
|
|
|
|
|
// Resolution order: package override > ecosystem override > global default.
|
|
|
|
|
func (c *Config) For(ecosystem, packagePURL string) time.Duration {
|
|
|
|
|
c.parse()
|
|
|
|
|
|
|
|
|
|
if d, ok := c.packageDurations[packagePURL]; ok {
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
if d, ok := c.ecosystemDurations[ecosystem]; ok {
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
return c.defaultDuration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsAllowed returns true if a version with the given publish time has passed
|
|
|
|
|
// the cooldown period for this ecosystem/package.
|
|
|
|
|
func (c *Config) IsAllowed(ecosystem, packagePURL string, publishedAt time.Time) bool {
|
|
|
|
|
d := c.For(ecosystem, packagePURL)
|
|
|
|
|
if d == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if publishedAt.IsZero() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return time.Since(publishedAt) >= d
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enabled returns true if any cooldown is configured.
|
|
|
|
|
func (c *Config) Enabled() bool {
|
|
|
|
|
c.parse()
|
|
|
|
|
if c.defaultDuration > 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
for _, d := range c.ecosystemDurations {
|
|
|
|
|
if d > 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, d := range c.packageDurations {
|
|
|
|
|
if d > 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseDuration parses a duration string supporting days (e.g., "3d"),
|
|
|
|
|
// in addition to Go's standard time.ParseDuration formats ("48h", "30m").
|
|
|
|
|
// "0" means disabled (returns 0).
|
|
|
|
|
func ParseDuration(s string) (time.Duration, error) {
|
|
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
|
if s == "" || s == "0" {
|
|
|
|
|
return 0, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle day suffix
|
|
|
|
|
if numStr, ok := strings.CutSuffix(s, "d"); ok {
|
|
|
|
|
days, err := strconv.ParseFloat(numStr, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, fmt.Errorf("invalid duration %q: %w", s, err)
|
|
|
|
|
}
|
2026-03-18 10:59:29 +00:00
|
|
|
return time.Duration(days * float64(hoursPerDay*time.Hour)), nil
|
2026-03-04 19:00:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
d, err := time.ParseDuration(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, fmt.Errorf("invalid duration %q: %w", s, err)
|
|
|
|
|
}
|
|
|
|
|
return d, nil
|
|
|
|
|
}
|