2026-09-20 14:42:30 +05:30
|
|
|
// Package denylist implements an immutable, exact-version package policy.
|
|
|
|
|
package denylist
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-10-31 09:49:35 +00:00
|
|
|
"github.com/git-pkgs/proxy/internal/packageurl"
|
2026-09-20 14:42:30 +05:30
|
|
|
"github.com/git-pkgs/purl"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Policy is safe for concurrent reads. A nil policy allows every version.
|
|
|
|
|
type Policy struct {
|
|
|
|
|
packages map[string]map[string]bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New validates and canonicalizes versioned PURLs. Qualifiers and subpaths are
|
2026-10-22 20:44:02 +02:00
|
|
|
// rejected because the proxy cannot reliably distinguish them at every endpoint,
|
|
|
|
|
// except a conda channel, which download URLs always name.
|
2026-09-20 14:42:30 +05:30
|
|
|
func New(packages []string) (*Policy, error) {
|
|
|
|
|
if len(packages) == 0 {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
p := &Policy{packages: make(map[string]map[string]bool)}
|
|
|
|
|
for _, value := range packages {
|
|
|
|
|
pkg, err := purl.Parse(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid denylist package %q: %w", value, err)
|
|
|
|
|
}
|
2026-10-22 20:44:02 +02:00
|
|
|
if pkg.Version == "" || !allowedQualifiers(pkg) || pkg.Subpath != "" ||
|
2026-09-20 14:42:30 +05:30
|
|
|
strings.ContainsAny(pkg.Namespace+pkg.Name+pkg.Version, "*?[]<>=| \t\r\n") {
|
|
|
|
|
return nil, fmt.Errorf("invalid denylist package %q: expected an exact versioned PURL without qualifiers or subpath", value)
|
|
|
|
|
}
|
2026-10-22 20:44:02 +02:00
|
|
|
id := identify(pkg)
|
|
|
|
|
key := id.key
|
|
|
|
|
if id.channel != "" {
|
|
|
|
|
key = id.channelKey()
|
|
|
|
|
}
|
2026-09-20 14:42:30 +05:30
|
|
|
if p.packages[key] == nil {
|
|
|
|
|
p.packages[key] = make(map[string]bool)
|
|
|
|
|
}
|
2026-10-22 20:44:02 +02:00
|
|
|
p.packages[key][id.version] = true
|
2026-11-08 08:04:09 +01:00
|
|
|
// Debian and RPM filenames leave the epoch out, so an entry with
|
|
|
|
|
// one also matches the version without it.
|
|
|
|
|
if pkg.Type == "deb" || pkg.Type == "rpm" {
|
|
|
|
|
if _, version, ok := strings.Cut(id.version, ":"); ok && version != "" {
|
|
|
|
|
p.packages[key][version] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-20 14:42:30 +05:30
|
|
|
}
|
|
|
|
|
return p, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 20:44:02 +02:00
|
|
|
func allowedQualifiers(pkg *purl.PURL) bool {
|
|
|
|
|
for _, q := range pkg.Qualifiers {
|
|
|
|
|
if pkg.Type != "conda" || q.Key != "channel" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 14:42:30 +05:30
|
|
|
// Denied reports whether a versioned PURL is denied.
|
|
|
|
|
func (p *Policy) Denied(value string) bool {
|
|
|
|
|
if p == nil || len(p.packages) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
pkg, err := purl.Parse(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-10-22 20:44:02 +02:00
|
|
|
id := identify(pkg)
|
|
|
|
|
versions := []string{id.version}
|
|
|
|
|
// A platform gem's version carries the platform ("1.16.0-x86_64-linux"),
|
|
|
|
|
// so also match the plain version.
|
|
|
|
|
if pkg.Type == "gem" {
|
|
|
|
|
if base, _, found := strings.Cut(id.version, "-"); found && base != "" {
|
|
|
|
|
versions = append(versions, base)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, version := range versions {
|
|
|
|
|
if p.packages[id.key][version] || (id.channel != "" && p.packages[id.channelKey()][version]) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
2026-09-20 14:42:30 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Versions returns a caller-owned set of denied versions for a package PURL.
|
|
|
|
|
func (p *Policy) Versions(packagePURL string) map[string]bool {
|
2026-10-22 20:44:02 +02:00
|
|
|
if p == nil || len(p.packages) == 0 {
|
2026-09-20 14:42:30 +05:30
|
|
|
return nil
|
|
|
|
|
}
|
2026-10-22 20:44:02 +02:00
|
|
|
pkg, err := purl.Parse(packagePURL)
|
|
|
|
|
if err != nil {
|
2026-09-20 14:42:30 +05:30
|
|
|
return nil
|
|
|
|
|
}
|
2026-10-22 20:44:02 +02:00
|
|
|
id := identify(pkg)
|
|
|
|
|
result := make(map[string]bool)
|
|
|
|
|
for version := range p.packages[id.key] {
|
2026-09-20 14:42:30 +05:30
|
|
|
result[version] = true
|
|
|
|
|
}
|
2026-10-22 20:44:02 +02:00
|
|
|
if id.channel != "" {
|
|
|
|
|
for version := range p.packages[id.channelKey()] {
|
|
|
|
|
result[version] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(result) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-09-20 14:42:30 +05:30
|
|
|
return result
|
|
|
|
|
}
|
2026-10-22 20:44:02 +02:00
|
|
|
|
|
|
|
|
// identity is the form a PURL is compared in: the same for denylist entries
|
|
|
|
|
// and for the PURLs handlers build, however each spells the package.
|
|
|
|
|
type identity struct {
|
|
|
|
|
key string // versionless PURL without channel
|
|
|
|
|
channel string // conda channel, if any
|
|
|
|
|
version string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (id identity) channelKey() string {
|
|
|
|
|
return id.key + "?channel=" + id.channel
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-23 19:29:00 +02:00
|
|
|
// genericSwiftRegistry returns the scope and name of a Swift registry archive,
|
|
|
|
|
// which the proxy stores as pkg:generic/swift-registry/{scope}.{name}.
|
|
|
|
|
func genericSwiftRegistry(pkg *purl.PURL) (scope, name string, ok bool) {
|
|
|
|
|
if pkg.Type != "generic" || !strings.EqualFold(pkg.Namespace, "swift-registry") {
|
|
|
|
|
return "", "", false
|
2026-10-23 09:55:06 +02:00
|
|
|
}
|
2026-10-23 19:29:00 +02:00
|
|
|
scope, name, found := strings.Cut(pkg.Name, ".")
|
|
|
|
|
return scope, name, found && scope != "" && name != ""
|
2026-10-23 09:55:06 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-23 19:29:00 +02:00
|
|
|
// identify compares PURLs in purl's comparison form. The generic
|
|
|
|
|
// swift-registry spelling is the proxy's own, so it is mapped here first.
|
2026-11-08 08:04:09 +01:00
|
|
|
// The namespace of a distro type names the distribution, which handlers
|
|
|
|
|
// include for some types and not others, so it is dropped as retention and
|
|
|
|
|
// the inventory do.
|
2026-10-22 20:44:02 +02:00
|
|
|
func identify(pkg *purl.PURL) identity {
|
2026-10-23 19:29:00 +02:00
|
|
|
if scope, name, ok := genericSwiftRegistry(pkg); ok {
|
2026-10-25 10:14:03 +00:00
|
|
|
pkg = purl.New("swift", scope, name, pkg.Version, nil)
|
2026-10-23 19:29:00 +02:00
|
|
|
}
|
|
|
|
|
c := pkg.ComparisonForm()
|
2026-10-22 20:44:02 +02:00
|
|
|
var channel string
|
2026-10-23 19:29:00 +02:00
|
|
|
if c.Type == "conda" {
|
|
|
|
|
// An entry without a channel matches every channel.
|
|
|
|
|
channel = c.Qualifier("channel")
|
2026-10-22 20:44:02 +02:00
|
|
|
}
|
2026-11-08 08:04:09 +01:00
|
|
|
namespace := c.Namespace
|
|
|
|
|
if packageurl.DistroType(c.Type) {
|
|
|
|
|
namespace = ""
|
|
|
|
|
}
|
|
|
|
|
key := purl.New(c.Type, namespace, c.Name, "", nil).String()
|
2026-10-31 09:49:35 +00:00
|
|
|
return identity{key: packageurl.PolicyPackageKey(key), channel: channel, version: c.Version}
|
2026-10-22 20:44:02 +02:00
|
|
|
}
|