Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/server/swift_sources.go
simon chrzanowski 1979fb098e
Accept Swift source repository PURLs in the denylist and retention rules (#641)
* Accept Swift source repository PURLs in the denylist and retention rules

Closes #613. pkg:swift/github.com/apple/swift-nio, as SBOM tools and GHSA
write Swift packages, is resolved to its registry identity through the
upstream's /identifiers:

- Denylist entries are resolved at startup, and the proxy refuses to start
  when one can't be, since a skipped entry would let the version through.
- Retention keys are accepted at validation and kept pending. The
  retention loop resolves them before each sweep, and Swift isn't swept
  while any is pending, so a rule that keeps a package is never skipped.

The mirror's source URL helper moves to the handler as SwiftSourceURL.

* Resolve Swift source denylist entries in proxy mirror too

serve resolved source-form Swift denylist entries through the upstream,
but proxy mirror built its denylist directly, so such an entry never
matched a mirror download. Both now use server.ConfigureDenylist, and
neither runs when an entry can't be resolved.

* Apply Swift source policies to every listed identity

A Swift registry may list several identities for one source repository,
but a denylist entry or retention rule naming the repository only
covered the first. Denying pkg:swift/github.com/acme/widget@1.0.0 let
acme.widget through when the registry listed other.widget first, and a
never-evict rule cleared the cached apple.swift-nio archive when
other.swift-nio came first.

Both now cover every identity the registry lists. A retention rule that
conflicts with another rule for any of them stays pending, with none
applied. Mirroring a source PURL still downloads the first identity.
2026-10-31 11:34:05 +00:00

136 lines
5 KiB
Go

package server
import (
"context"
"fmt"
"log/slog"
"maps"
"slices"
"strings"
"time"
"github.com/git-pkgs/proxy/internal/config"
"github.com/git-pkgs/proxy/internal/denylist"
"github.com/git-pkgs/proxy/internal/handler"
"github.com/git-pkgs/proxy/internal/retention"
"github.com/git-pkgs/purl"
)
// A Swift package can be configured by its source repository, as SBOM tools
// and GHSA name it (pkg:swift/github.com/apple/swift-nio), as well as by its
// registry identity. The proxy stores and compares registry identities, so
// source repositories are resolved through the upstream's /identifiers: for
// the denylist at startup, which fails when one can't be, and for retention
// before each sweep, which leaves Swift unswept until all are.
// sourceResolveTimeout bounds resolving the denylist's source repositories at
// startup.
const sourceResolveTimeout = time.Minute
// sourceResolver resolves a package PURL that names a package by its source
// to the stored package PURLs retention compares.
type sourceResolver func(ctx context.Context, p *purl.PURL) ([]string, error)
// swiftRetentionResolver resolves Swift source repositories through h, to
// every identity the upstream lists.
func swiftRetentionResolver(h *handler.SwiftHandler, reg *retention.Registry) sourceResolver {
return func(ctx context.Context, p *purl.PURL) ([]string, error) {
resolved, err := h.ResolveSourcePURL(ctx, p)
if err != nil {
return nil, err
}
canonical := make([]string, 0, len(resolved))
for _, r := range resolved {
c, ok := reg.Canonical(r)
if !ok {
return nil, fmt.Errorf("%s resolved to %s, which names no stored package", p, r)
}
canonical = append(canonical, c)
}
return canonical, nil
}
}
// retentionResolvers returns how the retention loop resolves package rules
// that name a package by its source, by ecosystem key.
func (s *Server) retentionResolvers(proxy *handler.Proxy) map[string]sourceResolver {
return map[string]sourceResolver{
"swift": swiftRetentionResolver(handler.NewSwiftHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.Swift), retention.Default),
}
}
// resolveDenylistSources returns entries with each Swift source repository
// replaced by every registry identity the upstream lists for it, keeping the
// version. An entry that can't be resolved is an error: left out, it would
// let the version through.
func resolveDenylistSources(ctx context.Context, h *handler.SwiftHandler, entries []string) ([]string, error) {
resolved := make([]string, 0, len(entries))
for _, entry := range entries {
p, err := purl.Parse(entry)
if err != nil || handler.SwiftSourcePURL(p) == "" {
resolved = append(resolved, entry)
continue
}
identities, err := h.ResolveSourcePURL(ctx, p)
if err != nil {
return nil, fmt.Errorf("resolving denylist package %q: %w", entry, err)
}
for _, identity := range identities {
resolved = append(resolved, identity.String())
}
}
return resolved, nil
}
// resolveRetentionSources resolves the package rules in rules.Sources it can,
// moving them to rules.Packages. A rule that fails stays pending, so its
// ecosystem isn't swept, and is tried again before the next sweep.
func resolveRetentionSources(ctx context.Context, logger *slog.Logger, resolvers map[string]sourceResolver, rules retention.Rules) {
for _, key := range slices.Sorted(maps.Keys(rules.Sources)) {
resolve := resolvers[key]
for _, source := range slices.Sorted(maps.Keys(rules.Sources[key])) {
if resolve == nil {
logger.Error("retention: no way to resolve package rule, not sweeping "+key, "package", source)
continue
}
p, err := purl.Parse(source)
if err != nil {
continue // config validation refuses these
}
canonical, err := resolve(ctx, p)
if err == nil {
err = rules.Resolve(key, source, canonical)
}
if err != nil {
logger.Warn("retention: could not resolve package rule, not sweeping "+key+" until it is",
"package", source, "error", err)
continue
}
logger.Info("retention: resolved package rule", "package", source, "stored", strings.Join(canonical, ", "))
}
}
}
// ConfigureDenylist sets proxy's denylist from cfg, resolving Swift source
// repositories through the upstream first. serve and proxy mirror call it
// once proxy can fetch metadata. An entry that can't be resolved is an
// error, so neither starts with a denylist that lets its versions through.
func ConfigureDenylist(cfg *config.Config, proxy *handler.Proxy) error {
entries := cfg.Denylist.Packages
// Report a malformed entry before asking the upstream about any.
if _, err := denylist.New(entries); err != nil {
return fmt.Errorf("configuring denylist: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), sourceResolveTimeout)
defer cancel()
entries, err := resolveDenylistSources(ctx, handler.NewSwiftHandler(proxy, cfg.BaseURL, cfg.Upstream.Swift), entries)
if err != nil {
return fmt.Errorf("configuring denylist: %w", err)
}
policy, err := denylist.New(entries)
if err != nil {
return fmt.Errorf("configuring denylist: %w", err)
}
proxy.Denylist = policy
return nil
}