mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 17:24:16 +00:00
- diff view gains a Packages tab with added/removed/changed/unchanged package tables and purl-derived type/license/upstream links - captain records verified against the DID's atcr_hold service before caching (processor + batch backfill), preventing forged holds - fix empty-handle updates clobbering cached handles and colliding on the UNIQUE constraint - move fillPrevCIDs into repo.go; DirectRepoOperator is now canonical, repomgr kept as a test oracle - surface read-only crew status in hold selector - reconcile docs
200 lines
5.3 KiB
Go
200 lines
5.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// purlInfo is the subset of a package URL (pkg:type/namespace/name@version)
|
|
// needed for display: the ecosystem type and an upstream link.
|
|
type purlInfo struct {
|
|
Type string // purl type, lowercased (e.g. "deb", "npm", "golang")
|
|
Namespace string // may be empty or multi-segment (golang)
|
|
Name string
|
|
Version string
|
|
Qualifiers map[string]string
|
|
}
|
|
|
|
// parsePurl parses a package URL (https://github.com/package-url/purl-spec):
|
|
// pkg:type/namespace/name@version?qualifiers#subpath
|
|
// Returns nil for anything malformed — callers fall back to supplier sniffing.
|
|
func parsePurl(s string) *purlInfo {
|
|
rest, ok := strings.CutPrefix(s, "pkg:")
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if i := strings.IndexByte(rest, '#'); i >= 0 {
|
|
rest = rest[:i]
|
|
}
|
|
|
|
qualifiers := map[string]string{}
|
|
if i := strings.IndexByte(rest, '?'); i >= 0 {
|
|
for kv := range strings.SplitSeq(rest[i+1:], "&") {
|
|
if k, v, ok := strings.Cut(kv, "="); ok && k != "" {
|
|
if uv, err := url.QueryUnescape(v); err == nil {
|
|
v = uv
|
|
}
|
|
qualifiers[strings.ToLower(k)] = v
|
|
}
|
|
}
|
|
rest = rest[:i]
|
|
}
|
|
|
|
typ, rest, ok := strings.Cut(strings.TrimPrefix(rest, "/"), "/")
|
|
if !ok || typ == "" {
|
|
return nil
|
|
}
|
|
|
|
version := ""
|
|
if i := strings.LastIndexByte(rest, '@'); i >= 0 {
|
|
version = rest[i+1:]
|
|
rest = rest[:i]
|
|
}
|
|
|
|
namespace := ""
|
|
name := rest
|
|
if i := strings.LastIndexByte(rest, '/'); i >= 0 {
|
|
namespace = rest[:i]
|
|
name = rest[i+1:]
|
|
}
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
|
|
unescape := func(s string) string {
|
|
if u, err := url.PathUnescape(s); err == nil {
|
|
return u
|
|
}
|
|
return s
|
|
}
|
|
return &purlInfo{
|
|
Type: strings.ToLower(typ),
|
|
Namespace: unescape(namespace),
|
|
Name: unescape(name),
|
|
Version: unescape(version),
|
|
Qualifiers: qualifiers,
|
|
}
|
|
}
|
|
|
|
// purlDisplayType maps purl types to the short labels the UI shows. Types
|
|
// without a friendlier label (deb, npm, gem, apk, rpm, nuget, ...) pass
|
|
// through unchanged; "generic" (binary catalogers) maps to empty so the
|
|
// column shows "-" rather than a meaningless badge.
|
|
func purlDisplayType(t string) string {
|
|
switch t {
|
|
case "golang":
|
|
return "go"
|
|
case "pypi":
|
|
return "python"
|
|
case "cargo":
|
|
return "rust"
|
|
case "maven":
|
|
return "java"
|
|
case "composer":
|
|
return "php"
|
|
case "generic":
|
|
return ""
|
|
default:
|
|
return t
|
|
}
|
|
}
|
|
|
|
// purlURL returns the canonical upstream page for a package, or "" when the
|
|
// ecosystem has no stable public index or the purl type is unknown.
|
|
func purlURL(p *purlInfo) string {
|
|
// Syft stamps "UNKNOWN" when a manifest has no version field. Published
|
|
// packages always have one (registries require it), so these entries are
|
|
// almost always phantom nested manifests (e.g. a dist/package.json inside
|
|
// another package's tarball) that don't exist upstream — don't link them.
|
|
if strings.EqualFold(p.Version, "unknown") {
|
|
return ""
|
|
}
|
|
|
|
esc := url.PathEscape
|
|
switch p.Type {
|
|
case "npm":
|
|
// Scoped names (@scope/name) are accepted literally by npmx.dev;
|
|
// versions use the /v/{version} path form (name@version 404s).
|
|
name := p.Name
|
|
if p.Namespace != "" {
|
|
name = p.Namespace + "/" + p.Name
|
|
}
|
|
if p.Version != "" {
|
|
return "https://npmx.dev/package/" + name + "/v/" + esc(p.Version)
|
|
}
|
|
return "https://npmx.dev/package/" + name
|
|
case "pypi":
|
|
if p.Version != "" {
|
|
return "https://pypi.org/project/" + esc(p.Name) + "/" + esc(p.Version) + "/"
|
|
}
|
|
return "https://pypi.org/project/" + esc(p.Name) + "/"
|
|
case "gem":
|
|
return "https://rubygems.org/gems/" + esc(p.Name)
|
|
case "golang":
|
|
path := p.Name
|
|
if p.Namespace != "" {
|
|
path = p.Namespace + "/" + p.Name
|
|
}
|
|
if p.Version != "" {
|
|
return "https://pkg.go.dev/" + path + "@" + esc(p.Version)
|
|
}
|
|
return "https://pkg.go.dev/" + path
|
|
case "cargo":
|
|
if p.Version != "" {
|
|
return "https://crates.io/crates/" + esc(p.Name) + "/" + esc(p.Version)
|
|
}
|
|
return "https://crates.io/crates/" + esc(p.Name)
|
|
case "nuget":
|
|
if p.Version != "" {
|
|
return "https://www.nuget.org/packages/" + esc(p.Name) + "/" + esc(p.Version)
|
|
}
|
|
return "https://www.nuget.org/packages/" + esc(p.Name)
|
|
case "maven":
|
|
if p.Namespace == "" {
|
|
return ""
|
|
}
|
|
u := "https://central.sonatype.com/artifact/" + esc(p.Namespace) + "/" + esc(p.Name)
|
|
if p.Version != "" {
|
|
u += "/" + esc(p.Version)
|
|
}
|
|
return u
|
|
case "composer":
|
|
if p.Namespace == "" {
|
|
return ""
|
|
}
|
|
return "https://packagist.org/packages/" + esc(p.Namespace) + "/" + esc(p.Name)
|
|
case "deb":
|
|
// Trackers are keyed by source package; Syft records it in the
|
|
// "upstream" qualifier for binary packages (e.g. libssl3t64 →
|
|
// openssl). Fall back to the binary name, which usually matches.
|
|
name := p.Name
|
|
if up := p.Qualifiers["upstream"]; up != "" {
|
|
if i := strings.IndexByte(up, '@'); i >= 0 {
|
|
up = up[:i]
|
|
}
|
|
name = up
|
|
}
|
|
switch p.Namespace {
|
|
case "debian":
|
|
return "https://tracker.debian.org/pkg/" + esc(name)
|
|
case "ubuntu":
|
|
return "https://launchpad.net/ubuntu/+source/" + esc(name)
|
|
}
|
|
return ""
|
|
case "apk":
|
|
return "https://pkgs.alpinelinux.org/packages?name=" + url.QueryEscape(p.Name)
|
|
case "rpm":
|
|
if p.Namespace == "fedora" {
|
|
return "https://packages.fedoraproject.org/pkgs/" + esc(p.Name) + "/"
|
|
}
|
|
return ""
|
|
case "github":
|
|
if p.Namespace == "" {
|
|
return ""
|
|
}
|
|
return "https://github.com/" + esc(p.Namespace) + "/" + esc(p.Name)
|
|
}
|
|
return ""
|
|
}
|