Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 07:39:19 -05:00
pkg-proxy/internal/denylist/denylist.go
Simon Chrzanowski a288afab32 Match denylist entries for distro packages as handlers name them
The denylist kept the PURL namespace, but the Debian and RPM handlers
build pkg:deb/{name} and pkg:rpm/{name} without a vendor, while Alpine
and Arch build pkg:apk/alpine/{name} and pkg:alpm/arch/{name} with one.
So standard entries such as pkg:deb/debian/curl@... or
pkg:rpm/fedora/curl@..., and pkg:apk/busybox@... or pkg:alpm/glibc@...,
never matched. Drop the namespace of distro types on both sides, as
retention and the inventory already do, using packageurl's list of
those types.

Debian and RPM handlers read versions from filenames, which leave out
the epoch, so an entry such as pkg:deb/foo@1:2.3-1 never matched
foo_2.3-1_amd64.deb. An entry with an epoch now also covers the
version without it.
2026-11-08 08:04:09 +01:00

157 lines
4.7 KiB
Go

// Package denylist implements an immutable, exact-version package policy.
package denylist
import (
"fmt"
"strings"
"github.com/git-pkgs/proxy/internal/packageurl"
"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
// rejected because the proxy cannot reliably distinguish them at every endpoint,
// except a conda channel, which download URLs always name.
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)
}
if pkg.Version == "" || !allowedQualifiers(pkg) || pkg.Subpath != "" ||
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)
}
id := identify(pkg)
key := id.key
if id.channel != "" {
key = id.channelKey()
}
if p.packages[key] == nil {
p.packages[key] = make(map[string]bool)
}
p.packages[key][id.version] = true
// 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
}
}
}
return p, nil
}
func allowedQualifiers(pkg *purl.PURL) bool {
for _, q := range pkg.Qualifiers {
if pkg.Type != "conda" || q.Key != "channel" {
return false
}
}
return true
}
// 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
}
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
}
// Versions returns a caller-owned set of denied versions for a package PURL.
func (p *Policy) Versions(packagePURL string) map[string]bool {
if p == nil || len(p.packages) == 0 {
return nil
}
pkg, err := purl.Parse(packagePURL)
if err != nil {
return nil
}
id := identify(pkg)
result := make(map[string]bool)
for version := range p.packages[id.key] {
result[version] = true
}
if id.channel != "" {
for version := range p.packages[id.channelKey()] {
result[version] = true
}
}
if len(result) == 0 {
return nil
}
return result
}
// 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
}
// 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
}
scope, name, found := strings.Cut(pkg.Name, ".")
return scope, name, found && scope != "" && name != ""
}
// identify compares PURLs in purl's comparison form. The generic
// swift-registry spelling is the proxy's own, so it is mapped here first.
// 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.
func identify(pkg *purl.PURL) identity {
if scope, name, ok := genericSwiftRegistry(pkg); ok {
pkg = purl.New("swift", scope, name, pkg.Version, nil)
}
c := pkg.ComparisonForm()
var channel string
if c.Type == "conda" {
// An entry without a channel matches every channel.
channel = c.Qualifier("channel")
}
namespace := c.Namespace
if packageurl.DistroType(c.Type) {
namespace = ""
}
key := purl.New(c.Type, namespace, c.Name, "", nil).String()
return identity{key: packageurl.PolicyPackageKey(key), channel: channel, version: c.Version}
}