mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-06-02 08:38:17 -04:00
Compare commits
1 commit
main
...
registries
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe89cbd3f1 |
16 changed files with 19 additions and 274 deletions
|
|
@ -283,9 +283,9 @@ HTTP server setup, web UI, and API handlers.
|
|||
|
||||
Prometheus metrics for cache performance, upstream latency, storage operations, and active requests. See the Monitoring section of the README for the full metric list.
|
||||
|
||||
### `internal/cooldown`
|
||||
### Cooldown
|
||||
|
||||
Version age filtering for supply chain attack mitigation. Configurable at global, ecosystem, and per-package levels. Supported by npm, PyPI, pub.dev, and Composer handlers.
|
||||
Version age filtering for supply chain attack mitigation, provided by [github.com/git-pkgs/cooldown](https://github.com/git-pkgs/cooldown). Configurable at global, ecosystem, and per-package levels. Supported by npm, PyPI, pub.dev, and Composer handlers.
|
||||
|
||||
### `internal/enrichment`
|
||||
|
||||
|
|
|
|||
3
go.mod
3
go.mod
|
|
@ -5,9 +5,10 @@ go 1.25.6
|
|||
require (
|
||||
github.com/CycloneDX/cyclonedx-go v0.10.0
|
||||
github.com/git-pkgs/archives v0.3.0
|
||||
github.com/git-pkgs/cooldown v0.1.1
|
||||
github.com/git-pkgs/enrichment v0.2.2
|
||||
github.com/git-pkgs/purl v0.1.12
|
||||
github.com/git-pkgs/registries v0.5.1
|
||||
github.com/git-pkgs/registries v0.6.0
|
||||
github.com/git-pkgs/spdx v0.1.3
|
||||
github.com/git-pkgs/vers v0.2.5
|
||||
github.com/git-pkgs/vulns v0.1.5
|
||||
|
|
|
|||
6
go.sum
6
go.sum
|
|
@ -252,6 +252,8 @@ github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1
|
|||
github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
|
||||
github.com/git-pkgs/archives v0.3.0 h1:iXKyO83jEFub1PGEDlHmk2tQ7XeV5LySTc0sEkH3x78=
|
||||
github.com/git-pkgs/archives v0.3.0/go.mod h1:LTJ1iQVFA7otizWMOyiI82NYVmyBWAPRzwu/e30rcXU=
|
||||
github.com/git-pkgs/cooldown v0.1.1 h1:9OqqzCB8gANz/y44SmqGD0Jp8Qtu81D1sCbKl6Ehg7w=
|
||||
github.com/git-pkgs/cooldown v0.1.1/go.mod h1:v7APuK/UouTiu8mWQZbdDmj7DfxxkGUeuhjaRB5gv9E=
|
||||
github.com/git-pkgs/enrichment v0.2.2 h1:vaQu5vs3tjQB5JI0gzBrUCynUc9z3l5byPhgKFaNZrc=
|
||||
github.com/git-pkgs/enrichment v0.2.2/go.mod h1:5JWGmlHWcv5HQHUrctcpnRUNpEF5VAixD2z4zvqKejs=
|
||||
github.com/git-pkgs/packageurl-go v0.3.1 h1:WM3RBABQZLaRBxgKyYughc3cVBE8KyQxbSC6Jt5ak7M=
|
||||
|
|
@ -260,8 +262,8 @@ github.com/git-pkgs/pom v0.1.4 h1:C6st+XSbF75eKuwfdkDZZtYHoTcaWRIEQYar5VtszUo=
|
|||
github.com/git-pkgs/pom v0.1.4/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk=
|
||||
github.com/git-pkgs/purl v0.1.12 h1:qCskrEU1LWQhCkIVZd992W5++Bsxazvx2Cx1/65qCvU=
|
||||
github.com/git-pkgs/purl v0.1.12/go.mod h1:ofp4mHsR0cUeVONQaf33n6Wxg2QTEvtUdRfCedI8ouA=
|
||||
github.com/git-pkgs/registries v0.5.1 h1:UPE42CyZAsOfqO3N5bDelu28wS4Ifx/aOj0XZS4qYeI=
|
||||
github.com/git-pkgs/registries v0.5.1/go.mod h1:BY0YW+V0WDGBMuDR2aSMR3NzOPFK4K+F3j6+ch+cq3M=
|
||||
github.com/git-pkgs/registries v0.6.0 h1:ttQC8via9XAoLk9vqysf0K7uWl1bAyHPBWRBavRpAqs=
|
||||
github.com/git-pkgs/registries v0.6.0/go.mod h1:BY0YW+V0WDGBMuDR2aSMR3NzOPFK4K+F3j6+ch+cq3M=
|
||||
github.com/git-pkgs/spdx v0.1.3 h1:YQou23mLfzbW//6JlHUuc5x1P5VNIIDSku5gvauf86I=
|
||||
github.com/git-pkgs/spdx v0.1.3/go.mod h1:4HGGWyC8tg4DjOhrtBTYl4Lu+5i2BFuauGX8zcVcYPg=
|
||||
github.com/git-pkgs/vers v0.2.5 h1:tDtUMik9Iw1lyPHdT5V6LXjLo9LsJc0xOawURz7ibQU=
|
||||
|
|
|
|||
|
|
@ -1,125 +0,0 @@
|
|||
package cooldown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const hoursPerDay = 24
|
||||
|
||||
// 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)
|
||||
}
|
||||
return time.Duration(days * float64(hoursPerDay*time.Hour)), nil
|
||||
}
|
||||
|
||||
d, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid duration %q: %w", s, err)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
package cooldown
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseDuration(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want time.Duration
|
||||
wantErr bool
|
||||
}{
|
||||
{"", 0, false},
|
||||
{"0", 0, false},
|
||||
{"3d", 3 * 24 * time.Hour, false},
|
||||
{"7d", 7 * 24 * time.Hour, false},
|
||||
{"14d", 14 * 24 * time.Hour, false},
|
||||
{"1.5d", 36 * time.Hour, false},
|
||||
{"48h", 48 * time.Hour, false},
|
||||
{"30m", 30 * time.Minute, false},
|
||||
{"1h30m", 90 * time.Minute, false},
|
||||
{"invalid", 0, true},
|
||||
{"d", 0, true},
|
||||
{"xd", 0, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got, err := ParseDuration(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseDuration(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||
continue
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("ParseDuration(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigFor(t *testing.T) {
|
||||
c := &Config{
|
||||
Default: "3d",
|
||||
Ecosystems: map[string]string{
|
||||
"npm": "7d",
|
||||
"cargo": "0",
|
||||
},
|
||||
Packages: map[string]string{
|
||||
"pkg:npm/lodash": "0",
|
||||
"pkg:npm/@babel/core": "14d",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
ecosystem string
|
||||
packagePURL string
|
||||
want time.Duration
|
||||
}{
|
||||
// Package override takes priority
|
||||
{"npm", "pkg:npm/lodash", 0},
|
||||
{"npm", "pkg:npm/@babel/core", 14 * 24 * time.Hour},
|
||||
// Ecosystem override
|
||||
{"npm", "pkg:npm/express", 7 * 24 * time.Hour},
|
||||
{"cargo", "pkg:cargo/serde", 0},
|
||||
// Global default
|
||||
{"pypi", "pkg:pypi/requests", 3 * 24 * time.Hour},
|
||||
{"pub", "pkg:pub/flutter", 3 * 24 * time.Hour},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := c.For(tt.ecosystem, tt.packagePURL)
|
||||
if got != tt.want {
|
||||
t.Errorf("For(%q, %q) = %v, want %v", tt.ecosystem, tt.packagePURL, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigIsAllowed(t *testing.T) {
|
||||
c := &Config{
|
||||
Default: "3d",
|
||||
Packages: map[string]string{
|
||||
"pkg:npm/lodash": "0",
|
||||
},
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ecosystem string
|
||||
packagePURL string
|
||||
publishedAt time.Time
|
||||
want bool
|
||||
}{
|
||||
{"old enough", "npm", "pkg:npm/express", now.Add(-4 * 24 * time.Hour), true},
|
||||
{"too recent", "npm", "pkg:npm/express", now.Add(-1 * 24 * time.Hour), false},
|
||||
{"exactly at boundary", "npm", "pkg:npm/express", now.Add(-3 * 24 * time.Hour), true},
|
||||
{"exempt package", "npm", "pkg:npm/lodash", now.Add(-1 * time.Minute), true},
|
||||
{"zero time", "npm", "pkg:npm/express", time.Time{}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := c.IsAllowed(tt.ecosystem, tt.packagePURL, tt.publishedAt)
|
||||
if got != tt.want {
|
||||
t.Errorf("IsAllowed(%q, %q, %v) = %v, want %v",
|
||||
tt.ecosystem, tt.packagePURL, tt.publishedAt, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigEnabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg Config
|
||||
want bool
|
||||
}{
|
||||
{"empty config", Config{}, false},
|
||||
{"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},
|
||||
{"all zero", Config{Default: "0", Ecosystems: map[string]string{"npm": "0"}}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.cfg.Enabled()
|
||||
if got != tt.want {
|
||||
t.Errorf("Enabled() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/git-pkgs/proxy/internal/cooldown"
|
||||
"github.com/git-pkgs/cooldown"
|
||||
)
|
||||
|
||||
func cargoTestProxy() *Proxy {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/git-pkgs/proxy/internal/cooldown"
|
||||
"github.com/git-pkgs/cooldown"
|
||||
)
|
||||
|
||||
func TestComposerRewriteMetadata(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/git-pkgs/proxy/internal/cooldown"
|
||||
"github.com/git-pkgs/cooldown"
|
||||
)
|
||||
|
||||
func TestCondaParseFilename(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/git-pkgs/proxy/internal/cooldown"
|
||||
"github.com/git-pkgs/cooldown"
|
||||
)
|
||||
|
||||
func TestGemParseFilename(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/git-pkgs/proxy/internal/cooldown"
|
||||
"github.com/git-pkgs/cooldown"
|
||||
"github.com/git-pkgs/proxy/internal/database"
|
||||
"github.com/git-pkgs/proxy/internal/metrics"
|
||||
"github.com/git-pkgs/proxy/internal/storage"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/git-pkgs/proxy/internal/cooldown"
|
||||
"github.com/git-pkgs/cooldown"
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/git-pkgs/proxy/internal/cooldown"
|
||||
"github.com/git-pkgs/cooldown"
|
||||
)
|
||||
|
||||
const testVersion100 = "1.0.0"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/git-pkgs/proxy/internal/cooldown"
|
||||
"github.com/git-pkgs/cooldown"
|
||||
)
|
||||
|
||||
func nugetTestProxy() *Proxy {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/git-pkgs/proxy/internal/cooldown"
|
||||
"github.com/git-pkgs/cooldown"
|
||||
)
|
||||
|
||||
func TestPubRewriteMetadata(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/git-pkgs/proxy/internal/cooldown"
|
||||
"github.com/git-pkgs/cooldown"
|
||||
"github.com/git-pkgs/registries/fetch"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ import (
|
|||
|
||||
swaggerdoc "github.com/git-pkgs/proxy/docs/swagger"
|
||||
"github.com/git-pkgs/proxy/internal/config"
|
||||
"github.com/git-pkgs/proxy/internal/cooldown"
|
||||
"github.com/git-pkgs/cooldown"
|
||||
"github.com/git-pkgs/proxy/internal/database"
|
||||
"github.com/git-pkgs/proxy/internal/enrichment"
|
||||
"github.com/git-pkgs/proxy/internal/handler"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue