From 4dfe593119191ad0a10710320b4c7c3aedd3c7bc Mon Sep 17 00:00:00 2001 From: Sivamuthu Kumar Date: Sat, 8 Aug 2026 11:15:08 -0400 Subject: [PATCH 1/2] cooldown: add package pattern overrides Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 11 ++++++++- cooldown.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++- cooldown_test.go | 28 +++++++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ff24c4b..f6946e6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A small, ecosystem-agnostic version-age filter for package-manager tools. Hides versions published too recently so the community has time to spot malicious releases before they're pulled into projects. -Cross-ecosystem by construction: the same `Config` shape covers npm, PyPI, Cargo, RubyGems, Composer, Conda, Hex, NuGet, Pub, and any future ecosystem. Resolution order is package-PURL > ecosystem-name > global default, so a single config can express a strict default with targeted opt-outs. +Cross-ecosystem by construction: the same `Config` shape covers npm, PyPI, Cargo, RubyGems, Composer, Conda, Hex, NuGet, Pub, and any future ecosystem. Resolution order is package-PURL > package-PURL pattern > ecosystem-name > global default, so a single config can express a strict default with targeted opt-outs. ## Install @@ -19,6 +19,9 @@ cfg := &cooldown.Config{ Packages: map[string]string{ // per-PURL override "pkg:npm/htmx.org": "0", // 0 = disabled }, + PackagePatterns: map[string]string{ // per-PURL glob override + "pkg:npm/@example/*": "0", + }, } if cfg.IsAllowed("npm", "pkg:npm/lodash", publishedAt) { @@ -28,6 +31,12 @@ if cfg.IsAllowed("npm", "pkg:npm/lodash", publishedAt) { `Config.For(ecosystem, purl)` returns the effective duration; useful when surfacing the policy to a UI. `Config.Enabled()` reports whether any cooldown is configured (cheap check before walking a large version set). +`PackagePatterns` uses Go path globs against versionless PURLs. For example, +`pkg:npm/@example/*` matches an npm scope. Exact `Packages` entries take +precedence over patterns. When multiple patterns match, the most specific +pattern wins; ties use lexical order. Scoped npm patterns accept `@` and match +canonical PURLs used by registry integrations. + Duration strings accept Go's standard formats (`48h`, `30m`, `1h30m`) plus a `d` suffix for days (`3d`). `0` disables the window. ## Why standalone diff --git a/cooldown.go b/cooldown.go index f37a2b9..fbcd5a2 100644 --- a/cooldown.go +++ b/cooldown.go @@ -2,6 +2,8 @@ package cooldown import ( "fmt" + "path" + "sort" "strconv" "strings" "time" @@ -24,12 +26,22 @@ type Config struct { // Keys are PURLs (e.g., "pkg:npm/lodash", "pkg:npm/@babel/core"). Packages map[string]string `json:"packages" yaml:"packages"` + // PackagePatterns overrides the cooldown for packages whose PURLs match a glob. + // Exact package overrides take precedence over matching patterns. + PackagePatterns map[string]string `json:"package_patterns" yaml:"package_patterns"` + defaultDuration time.Duration ecosystemDurations map[string]time.Duration packageDurations map[string]time.Duration + packagePatterns []packagePattern parsed bool } +type packagePattern struct { + glob string + duration time.Duration +} + // parse resolves all string durations into time.Duration values. // Called lazily on first use. func (c *Config) parse() { @@ -51,22 +63,61 @@ func (c *Config) parse() { d, _ := ParseDuration(v) c.packageDurations[k] = d } + + c.packagePatterns = make([]packagePattern, 0, len(c.PackagePatterns)) + for glob, value := range c.PackagePatterns { + if _, err := path.Match(glob, ""); err != nil { + continue + } + duration, err := ParseDuration(value) + if err != nil { + continue + } + c.packagePatterns = append(c.packagePatterns, packagePattern{glob: glob, duration: duration}) + } + sort.Slice(c.packagePatterns, func(i, j int) bool { + left, right := literalLength(c.packagePatterns[i].glob), literalLength(c.packagePatterns[j].glob) + if left != right { + return left > right + } + return c.packagePatterns[i].glob < c.packagePatterns[j].glob + }) +} + +func literalLength(glob string) int { + return len(glob) - strings.Count(glob, "*") - strings.Count(glob, "?") } // For returns the effective cooldown duration for a given ecosystem and package PURL. -// Resolution order: package override > ecosystem override > global default. +// Resolution order: package override > package pattern > ecosystem override > +// global default. func (c *Config) For(ecosystem, packagePURL string) time.Duration { c.parse() if d, ok := c.packageDurations[packagePURL]; ok { return d } + for _, candidate := range c.packagePatterns { + if matchesPattern(candidate.glob, packagePURL) { + return candidate.duration + } + } if d, ok := c.ecosystemDurations[ecosystem]; ok { return d } return c.defaultDuration } +func matchesPattern(glob, packagePURL string) bool { + matched, _ := path.Match(glob, packagePURL) + if matched { + return true + } + decoded := strings.ReplaceAll(packagePURL, "%40", "@") + matched, _ = path.Match(glob, decoded) + return matched +} + // 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 { @@ -96,6 +147,11 @@ func (c *Config) Enabled() bool { return true } } + for _, candidate := range c.packagePatterns { + if candidate.duration > 0 { + return true + } + } return false } diff --git a/cooldown_test.go b/cooldown_test.go index c366077..5d81ea2 100644 --- a/cooldown_test.go +++ b/cooldown_test.go @@ -119,6 +119,7 @@ func TestConfigEnabled(t *testing.T) { {"default only", Config{Default: "3d"}, true}, {"ecosystem only", Config{Ecosystems: map[string]string{"npm": "7d"}}, true}, {"package only", Config{Packages: map[string]string{"pkg:npm/x": "1d"}}, true}, + {"package pattern only", Config{PackagePatterns: map[string]string{"pkg:npm/@example/*": "1d"}}, true}, {"all zero", Config{Default: "0", Ecosystems: map[string]string{"npm": "0"}}, false}, } @@ -131,3 +132,30 @@ func TestConfigEnabled(t *testing.T) { }) } } + +func TestConfigPackagePatterns(t *testing.T) { + now := time.Now() + cfg := Config{ + Default: "7d", + PackagePatterns: map[string]string{ + "pkg:npm/@example/*": "0", + "pkg:npm/@example/critical": "2d", + }, + Packages: map[string]string{ + "pkg:npm/@example/critical": "4d", + }, + } + + if got := cfg.For("npm", "pkg:npm/@example/widget"); got != 0 { + t.Errorf("scoped pattern duration = %v, want 0", got) + } + if got := cfg.For("npm", "pkg:npm/@example/critical"); got != 4*24*time.Hour { + t.Errorf("exact package duration = %v, want 4d", got) + } + if !cfg.IsAllowed("npm", "pkg:npm/@example/widget", now) { + t.Fatal("matching package pattern should disable cooldown") + } + if cfg.IsAllowed("npm", "pkg:npm/public-package", now) { + t.Fatal("non-matching package should use default cooldown") + } +} From b2c20c6175853b35e25ce6c6b56716d62b5f17e3 Mon Sep 17 00:00:00 2001 From: Sivamuthu Kumar <60989380+ksivamuthu-cei@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:27:46 -0400 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cooldown.go | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/cooldown.go b/cooldown.go index fbcd5a2..f374b1f 100644 --- a/cooldown.go +++ b/cooldown.go @@ -85,7 +85,38 @@ func (c *Config) parse() { } func literalLength(glob string) int { - return len(glob) - strings.Count(glob, "*") - strings.Count(glob, "?") + // Count bytes that must match literally. This is used only for ordering patterns + // when multiple patterns match. + lit := 0 + inClass := false + escaped := false + for i := 0; i < len(glob); i++ { + c := glob[i] + if escaped { + lit++ + escaped = false + continue + } + if c == '\\' { + escaped = true + continue + } + if inClass { + if c == ']' { + inClass = false + } + continue + } + switch c { + case '*', '?': + // wildcard + case '[': + inClass = true + default: + lit++ + } + } + return lit } // For returns the effective cooldown duration for a given ecosystem and package PURL.