Skip to content
Open
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/quay/claircore
go 1.26.0

require (
github.com/CycloneDX/cyclonedx-go v0.12.0
github.com/Masterminds/semver v1.5.0
github.com/cespare/xxhash/v2 v2.3.0
github.com/doug-martin/goqu/v8 v8.6.0
Expand Down Expand Up @@ -59,7 +60,7 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
golang.org/x/mod v0.39.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
Expand Down
30 changes: 16 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
github.com/CycloneDX/cyclonedx-go v0.12.0 h1:/7Jum36UA6V043tQZ/fE3jf+Nf9gn/qxUFfd7QReMy8=
github.com/CycloneDX/cyclonedx-go v0.12.0/go.mod h1:V2577HhxDDCDLYfkm55WJrz16nHTfyQZwcWUBSG7Z28=
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
Expand All @@ -10,6 +12,8 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -55,10 +59,6 @@ github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GX
github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8=
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d h1:X4cedH4Kn3JPupAwwWuo4AzYp16P0OyLO9d7OnMZc/c=
github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d/go.mod h1:o8sgWoz3JADecfc/cTYD92/Et1yMqMy0utV1z+VaZao=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
Expand Down Expand Up @@ -95,8 +95,6 @@ github.com/regclient/regclient v0.11.5 h1:OHRsXO0F3qHGfa4HEUv+EkMH9NXNcCTBKjNzyC
github.com/regclient/regclient v0.11.5/go.mod h1:DZUOfIT14WFTK2Pj4vjd93avy9O4Fdpjrf9ir23TbRE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg=
Expand All @@ -106,12 +104,20 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/sudo-bmitch/oci-digest v0.1.2 h1:are0qzWTsFZGZ3Uvdi9OSztJszSWaab6iqquMEEB7rw=
github.com/sudo-bmitch/oci-digest v0.1.2/go.mod h1:SH6l5OIe0islKBZBedjiPOeET/0QwGL+/oYfQt51uQo=
github.com/terminalstatic/go-xsd-validate v0.1.8 h1:UVrTCy1j3DhwaYTTUF+QYO/Nan13S0tf+Jwi+p45Bf0=
github.com/terminalstatic/go-xsd-validate v0.1.8/go.mod h1:1kb47fi2c6onlf+B7UrrQ9VYraOhcYwFm3iG+J6F4Zo=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
Expand All @@ -126,8 +132,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
Expand Down Expand Up @@ -171,12 +177,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.35.0 h1:F+TUsmw09QxLzmi3aeYYGxjAXarmZaKgj3mKQHNaA8w=
Expand Down
296 changes: 296 additions & 0 deletions java/bom/bom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
// Package bom implements consuming Red Hat's java-flavored BOMs.
package bom

import (
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"iter"
"log/slog"
"slices"
"strings"
"unique"

"github.com/CycloneDX/cyclonedx-go"
"github.com/package-url/packageurl-go"

"github.com/quay/claircore/internal/zreader"
"github.com/quay/claircore/toolkit/types/cpe"
)

// ErrBadFormat is a sentinel error for Red Hat-flavor requirement checks.
var ErrBadFormat = errors.New("bad format")

func formatError(format string, a ...any) error {
return &errorFormat{inner: fmt.Errorf(format, a...)}
}

type errorFormat struct {
inner error
}

func (e *errorFormat) Error() string {
return fmt.Sprintf("java/bom: bad format: %v", e.inner)
}

func (e *errorFormat) Unwrap() error {
return e.inner
}

func (e *errorFormat) Is(tgt error) bool {
return tgt == ErrBadFormat
}

// LoadCDX loads the Red Hat-Java-flavored CycloneDX JSON SBoM.
// A compressed document is decompressed before parsing.
func LoadCDX(ctx context.Context, r io.ReaderAt) (iter.Seq2[Package, error], error) {
in, err := zreader.Reader(io.NewSectionReader(r, 0, -1))
if err != nil {
return nil, fmt.Errorf("java/bom: loading CycloneDX: %w", err)
}
defer in.Close()
var doc cyclonedx.BOM
dec := cyclonedx.NewBOMDecoder(in, cyclonedx.BOMFileFormatJSON)
if err := dec.Decode(&doc); err != nil {
return nil, fmt.Errorf("java/bom: loading CycloneDX: %w", err)
}
root := doc.Metadata.Component
if root == nil {
return nil, formatError("no root component")
}
if doc.Components == nil || len(*doc.Components) == 0 {
return nil, formatError("no components")
}
if doc.Dependencies == nil || len(*doc.Dependencies) == 0 {
return nil, formatError("no dependencies")
}
cmps := *doc.Components
deps := *doc.Dependencies
depidx := slices.IndexFunc(deps, func(d cyclonedx.Dependency) bool {
return d.Ref == root.BOMRef
})
if depidx == -1 {
return nil, fmt.Errorf("java/bom: missing dependencies of root component %q", root.BOMRef)
}

// Product frameworks (scope excluded, with a CPE) attribute Maven purls
// through their provides lists. A document with none of those keeps the
// single root CPE on each direct dependency.
frameworks := make(map[string][]cpe.WFN)
for i := range cmps {
cm := &cmps[i]
if !excludedFramework(cm) {
continue
}
wfns, err := productWFNs(cm)
if err != nil {
return nil, fmt.Errorf("java/bom: framework %q: %w", cm.BOMRef, err)
}
if len(wfns) == 0 {
continue
}
frameworks[cm.BOMRef] = wfns
}
var rootWFNs []cpe.WFN
attr := map[string][]cpe.WFN{}
if len(frameworks) == 0 {
wfn, err := cpe.Unbind(root.CPE)
if err != nil {
return nil, fmt.Errorf("java/bom: root component %q: %w", root.BOMRef, err)
}
rootWFNs = []cpe.WFN{wfn}
} else {
for i := range deps {
d := &deps[i]
wfns, ok := frameworks[d.Ref]
if !ok || d.Provides == nil {
continue
}
for _, ref := range *d.Provides {
attr[ref] = appendWFNs(attr[ref], wfns)
}
}
}

seq := func(yield func(Package, error) bool) {
var skip skipPurls
defer func() {
if skip.count == 0 {
return
}
slog.DebugContext(ctx, "skipped some purls", "skipped", &skip)
}()
c := make(map[string]*cyclonedx.Component, len(cmps))
for i := range cmps {
r := &cmps[i]
c[r.BOMRef] = r
}
emit := func(cm *cyclonedx.Component, cpes []cpe.WFN) bool {
if cm == nil {
return yield(Package{}, errors.New("java/bom: missing component"))
}
if !strings.HasPrefix(cm.PackageURL, `pkg:maven/`) {
skip.Add(cm.PackageURL)
return true
}
purl, err := packageurl.FromString(cm.PackageURL)
if err != nil {
return yield(Package{}, fmt.Errorf("java/bom: component %q: %w", cm.BOMRef, err))
}
var hashes map[unique.Handle[string]][]byte
if cm.Hashes != nil {
hs := *cm.Hashes
hashes = make(map[unique.Handle[string]][]byte, len(hs))
for _, h := range hs {
k, known := hashnames[h.Algorithm]
if !known {
continue
}
// Assume everything is encoded as hex:
v, err := hex.DecodeString(h.Value)
if err != nil {
return yield(Package{}, fmt.Errorf("java/bom: component %q: %w", cm.BOMRef, err))
}
hashes[k] = v
}
}
var loc string
if ev := cm.Evidence; ev != nil {
if ocs := ev.Occurrences; ocs != nil {
for _, oc := range *ocs {
if oc.Location != "" {
loc = oc.Location
break
}
}
}
}
ok := yield(Package{
CPEs: cpes,
PURL: purl,
Hashes: hashes,
Location: loc,
}, nil)
if !ok {
return false
}
if err := ctx.Err(); err != nil {
yield(Package{}, context.Cause(ctx))
return false
}
return true
}

if len(frameworks) == 0 {
dep := &deps[depidx]
if dep.Dependencies == nil {
return
}
for _, ref := range *dep.Dependencies {
if !strings.HasPrefix(ref, `pkg:maven/`) {
skip.Add(ref)
continue
}
if !emit(c[ref], rootWFNs) {
return
}
}
return
}
for i := range cmps {
cm := &cmps[i]
if cm.BOMRef == root.BOMRef || excludedFramework(cm) {
continue
}
if !emit(cm, attr[cm.BOMRef]) {
return
}
}
}

return seq, nil
}

func excludedFramework(cm *cyclonedx.Component) bool {
return cm.Type == cyclonedx.ComponentTypeFramework && cm.Scope == cyclonedx.ScopeExcluded
}

// productWFNs returns the product CPEs on a framework component.
// component.cpe is first. Additional CPEs come from evidence.identity.
func productWFNs(cm *cyclonedx.Component) ([]cpe.WFN, error) {
var out []cpe.WFN
add := func(s string) error {
if s == "" {
return nil
}
w, err := cpe.Unbind(s)
if err != nil {
return err
}
out = appendWFNs(out, []cpe.WFN{w})
return nil
}
if err := add(cm.CPE); err != nil {
return nil, err
}
if cm.Evidence == nil || cm.Evidence.Identity == nil {
return out, nil
}
id := cm.Evidence.Identity
var ids []cyclonedx.EvidenceIdentity
if id.Identities != nil {
ids = append(ids, (*id.Identities)...)
}
if id.Identity != nil {
ids = append(ids, *id.Identity)
}
for _, ident := range ids {
if ident.Field != cyclonedx.EvidenceIdentityFieldTypeCPE {
continue
}
if err := add(ident.ConcludedValue); err != nil {
return nil, err
}
}
return out, nil
}

func appendWFNs(dst, src []cpe.WFN) []cpe.WFN {
seen := make(map[string]struct{}, len(dst)+len(src))
for _, w := range dst {
seen[w.String()] = struct{}{}
}
for _, w := range src {
s := w.String()
if _, ok := seen[s]; ok {
continue
}
seen[s] = struct{}{}
dst = append(dst, w)
}
return dst
}

var _ slog.LogValuer = (*skipPurls)(nil)

type skipPurls struct {
examples []string
count int
}

func (s *skipPurls) Add(p string) {
if len(s.examples) < 16 {
s.examples = append(s.examples, p)
}
s.count++
}

// LogValue implements [slog.LogValuer].
func (s *skipPurls) LogValue() slog.Value {
return slog.GroupValue(
slog.Int("count", s.count),
slog.Any("examples", s.examples),
)
}
Loading
Loading